diff --git a/.claude/skills/feynman-auditor/SKILL.md b/.claude/skills/feynman-auditor/SKILL.md new file mode 100644 index 00000000..a8c8aee2 --- /dev/null +++ b/.claude/skills/feynman-auditor/SKILL.md @@ -0,0 +1,972 @@ +--- +name: feynman-auditor +description: Deep business logic bug finder using the Feynman technique. Language-agnostic — works on Solidity, Move, Rust, Go, C++, or any codebase. Questions every line, every ordering choice, every guard presence/absence, and every implicit assumption to surface logic bugs that pattern-matching misses. Triggers on /feynman, feynman audit, or deep logic review. +--- + +# Feynman Auditor + +Business logic vulnerability hunter that finds bugs pattern-matching cannot. Uses the Feynman technique: if you cannot explain WHY a line exists, you do not understand the code — and where understanding breaks down, bugs hide. + +**Language-agnostic by design.** Logic bugs live in the reasoning, not the syntax. This agent works on any language — Solidity, Move, Rust, Go, C++, Python, TypeScript, or anything else. The questions are universal; only the examples change. + +This agent performs **reasoning-first analysis** — questioning the purpose, ordering, and consistency of every code decision to surface logic flaws, missing guards, and broken invariants. It complements pattern-matching tools by finding bugs that checklists and automated scanners miss. + +## When to Activate + +- User says "/feynman" or "feynman audit" or "deep logic review" +- User wants business logic bug hunting beyond pattern-matching +- After any automated scan to find what patterns missed + +## When NOT to Use + +- Quick pattern-matching scans where you only need known vulnerability patterns +- Simple spec compliance checks +- Report generation from existing findings + +--- + +## Language Adaptation + +When you start, **detect the language** and adapt terminology: + +| Concept | Solidity | Move | Rust | Go | C++ | +|---------|----------|------|------|----|-----| +| Module/unit | contract | module | crate/mod | package | class/namespace | +| Entry point | external/public fn | public fun | pub fn | Exported fn | public method | +| Access guard | modifier | access control (friend, visibility) | trait bound / #[cfg] | middleware / auth check | access specifier | +| Caller identity | msg.sender | &signer | caller param / Context | ctx / request.User | this / session | +| Error/abort | revert / require | abort / assert! | panic! / Result::Err | error / panic | throw / exception | +| State storage | storage variables | global storage / resources | struct fields / state | struct fields / DB | member variables | +| Checked math | SafeMath / checked | built-in overflow abort | checked_add / saturating | math/big / overflow check | safe int libs | +| Test framework | Foundry / Hardhat | Move Prover / aptos move test | cargo test | go test | gtest / catch2 | +| Value/assets | ETH, ERC-20, NFTs | APT, Coin\, tokens | SOL, SPL tokens, funds | any value type | any value type | + +**IMPORTANT:** Do NOT force Solidity terminology onto non-Solidity code. Use the language's native concepts. The questions stay the same — the vocabulary adapts. + +--- + +## Core Philosophy + +``` +"What I cannot create, I do not understand." — Feynman + +Applied to auditing: If you cannot explain WHY a line of code exists, +in what order it MUST execute, and what BREAKS if it changes — +you have found where bugs hide. +``` + +Pattern matchers find KNOWN bug classes. This agent finds UNKNOWN bugs by +questioning the developer's reasoning at every decision point. + +--- + +## Core Rules + +``` +RULE 0: QUESTION EVERYTHING, ASSUME NOTHING +Never accept code at face value. Every line exists because a developer +made a decision. Your job is to question that decision. + +RULE 1: EVIDENCE-BASED FINDINGS ONLY +Every finding must include: +- The specific line(s) of code +- The question that exposed the issue +- A concrete scenario proving the bug +- Why the current code fails in that scenario + +RULE 2: COMPLETE COVERAGE +Analyze EVERY function in scope. Do not skip "simple" functions. +Business logic bugs hide in the code everyone assumes is correct. + +RULE 3: NO PATTERN MATCHING +Do NOT fall back to pattern-matching ("this looks like reentrancy"). +Reason from first principles about what this specific code does. + +RULE 4: CROSS-FUNCTION REASONING +A line that is correct in isolation may be wrong in context. +Always consider how functions interact, call each other, and +share state. +``` + +--- + +## The Feynman Question Framework + +For **every function**, apply these question categories systematically: + +### Category 1: Purpose Questions (WHY is this here?) + +For each line or block of code, ask: + +``` +Q1.1: Why does this line exist? What invariant does it protect? + → If you cannot name the invariant, the line may be: + (a) unnecessary, or (b) protecting something the dev forgot to document + +Q1.2: What happens if I DELETE this line entirely? + → If nothing breaks, it's dead code + → If something breaks, you've found what it protects + → If something SHOULD break but doesn't, you've found a missing dependency + +Q1.3: What SPECIFIC attack or edge case motivated this check? + → If the dev added a guard like `assert(amount > 0)`, what goes + wrong at amount=0? Trace the zero/empty/max value through + the entire function. + → Language examples: + Solidity: require(amount > 0) + Move: assert!(amount > 0, ERROR_ZERO) + Rust: ensure!(amount > 0, Error::Zero) + Go: if amount <= 0 { return ErrZero } + +Q1.4: Is this check SUFFICIENT for what it's trying to prevent? + → A check for `amount > 0` doesn't prevent dust/minimum-value griefing + → A check for `caller == owner` doesn't prevent owner key compromise + → A bounds check doesn't prevent off-by-one within the bounds +``` + +### Category 2: Ordering Questions (WHAT IF I MOVE THIS?) + +For each state-changing operation, ask: + +``` +Q2.1: What if this line executes BEFORE the line above it? + → Would a different ordering allow state manipulation? + → Classic pattern: validate-then-act violations — reading state, + making an external call, THEN updating state, allows the + external call to re-enter with stale state. + +Q2.2: What if this line executes AFTER the line below it? + → Does delaying this operation create a window of inconsistent state? + → Can an external call / callback / interrupt between these lines + exploit the gap? + +Q2.3: What is the FIRST line that changes state? What is the LAST line + that reads state? Is there a gap between them? + → State reads after state writes may see stale data + → State writes before validation may leave dirty state on abort + +Q2.4: If this function ABORTS HALFWAY through, what state is left behind? + → Are there side effects that persist despite the abort? + (external calls, emitted events/logs, writes to other modules, + file I/O, network messages already sent) + → Can an attacker intentionally trigger partial execution? + +Q2.5: Can the ORDER in which users call this function matter? + → Front-running / race conditions: does calling first give advantage? + → Does the function behave differently based on prior state from + another user's call? + → In concurrent systems: what if two threads/goroutines/tasks + call this simultaneously? +``` + +### Category 3: Consistency Questions (WHY does A have it but B doesn't?) + +Compare functions that SHOULD be symmetric: + +``` +Q3.1: If functionA has an access guard and functionB doesn't, WHY? + → Is functionB intentionally unrestricted, or did the dev forget? + → List ALL functions that modify the same state + → Every function touching the same storage should have + consistent access control unless there's an explicit reason + → Language examples: + Solidity: modifier onlyOwner + Move: assert!(signer::address_of(account) == @admin) + Rust: #[access_control(ctx.accounts.authority)] + Go: if !isAuthorized(ctx) { return ErrUnauthorized } + +Q3.2: If deposit() checks X, does withdraw() also check X? + → Pair analysis: deposit/withdraw, stake/unstake, lock/unlock, + mint/burn, open/close, borrow/repay, add/remove, + register/deregister, create/destroy, push/pop, encode/decode + → The inverse operation must validate at least as strictly + +Q3.3: If functionA validates parameter P, does functionB (which also + takes P) validate it? + → Same parameter, different validation = one of them is wrong + +Q3.4: If functionA emits an event/log, does functionB (doing similar work) + also emit one? + → Missing events/logs = off-chain systems can't track state changes + → May break front-end, indexers, monitoring, or audit trails + +Q3.5: If functionA uses overflow-safe arithmetic, does functionB? + → Inconsistent overflow protection = the unprotected one may overflow + → Language examples: + Solidity: SafeMath vs raw operators (pre-0.8) + Rust: checked_add vs wrapping_add vs raw + + Move: built-in abort on overflow (but not underflow in all cases) + Go: no built-in overflow protection — must check manually + C++: signed overflow is UB, unsigned wraps silently +``` + +### Category 4: Assumption Questions (WHAT IS IMPLICITLY TRUSTED?) + +Expose hidden assumptions: + +``` +Q4.1: What does this function assume about THE CALLER? + → Who can call this? Is that enforced or just assumed? + → Could the caller be a different type than expected? + Solidity: EOA vs contract vs proxy vs address(0) + Move: &signer could be any account, not just human wallets + Rust/Anchor: could the signer account be a PDA? + Go: could the HTTP caller be unauthenticated / spoofed? + C++: could this be called from a different thread? + → What if the caller IS the system itself? (self-calls, recursion) + +Q4.2: What does this function assume about EXTERNAL DATA it receives? + → For tokens/coins: standard behavior? Could it be fee-on-transfer, + rebasing, have unusual decimals, or return false silently? + → For API responses: always well-formed? What if malformed, empty, + or adversarially crafted? + → For user input: sanitized? What about injection, encoding tricks, + or type confusion? + → For deserialized data: trusted format? What if the schema changed + or the data was tampered with? + +Q4.3: What does this function assume about the current state? + → "This will never be called when paused/locked" — but IS it enforced? + → "Balance will always be sufficient" — but who guarantees that? + → "This map/vector will never be empty" — but what if it is? + → "This was already initialized" — but what if it wasn't? + +Q4.4: What does this function assume about TIME or ORDERING? + → Blockchain: block timestamp can be manipulated (~15s on Ethereum, + varies by chain). Move: epoch-based timing. Solana: slot-based. + → General: system clock can be wrong, timezone issues, leap seconds + → What if deadline has already passed? What if time = 0? + → What if events arrive out of order? (network, async, concurrent) + +Q4.5: What does this function assume about PRICES, RATES, or EXTERNAL VALUES? + → Can the value be manipulated within the same transaction/call? + → Is the data source fresh? What if the oracle/API is stale or dead? + → What if the value is 0? What if it's MAX_VALUE for the type? + → What if precision differs between source and consumer? + +Q4.6: What does this function assume about INPUT AMOUNTS or SIZES? + → What if amount/size = 0? What if it's the maximum representable value? + → What if amount = 1 (dust / minimum unit)? + → What if amount exceeds what's available? + → What if a collection is empty? What if it has millions of entries? +``` + +### Category 5: Boundary & Edge Case Questions (WHAT BREAKS AT THE EDGES?) + +``` +Q5.1: What happens on the FIRST call to this function? (Empty state) + → First depositor, first user, first initialization + → Division by zero when total = 0? + → Share/ratio inflation when pool/collection is empty? + → Uninitialized state treated as valid? + +Q5.2: What happens on the LAST call? (Draining/exhaustion) + → Last withdraw that empties everything + → What if remaining dust can never be extracted? + → Does rounding trap value permanently? + → What if the last element removal breaks an invariant? + +Q5.3: What if this function is called TWICE in rapid succession? + → Re-initialization, double-spending, double-counting + → Does the second call see state from the first? + → In concurrent systems: race condition between the two calls? + → Blockchain: two calls in the same block/transaction + +Q5.4: What if two DIFFERENT functions are called in the same context? + → Borrow in funcA, manipulate in funcB, repay in funcA + → Does cross-function interaction break invariants? + → What about callback patterns where control flow is non-linear? + +Q5.5: What if this function is called with THE SYSTEM ITSELF as a parameter? + → Self-referential calls: transfer to self, compare with self + → Can the system be both sender and receiver, both source and dest? + → What about circular references or recursive structures? +``` + +### Category 6: Return Value & Error Path Questions + +``` +Q6.1: What does this function return? Who consumes the return value? + → If the caller ignores the return value, what's lost? + → If the return value is wrong, what downstream logic breaks? + → Language-specific: Does the language even FORCE you to check? + Rust: Result must be used. Go: error can be silently ignored with _. + Solidity: low-level call returns bool that's often unchecked. + C++: [[nodiscard]] is opt-in. Move: values must be consumed. + +Q6.2: What happens on the ERROR/ABORT path? + → Are there side effects before the error? + → Does the error message leak sensitive information? + → Can an attacker cause targeted errors (griefing / DoS)? + → In languages with exceptions: is cleanup code (finally/defer/ + Drop) correct? Are resources leaked on the error path? + +Q6.3: What if an EXTERNAL CALL in this function fails silently? + → Does the language/runtime guarantee failure propagation? + → Is the error checked, or can it be swallowed? + → Language examples: + Solidity: low-level call returns (bool, bytes) — often unchecked + Go: err is a normal return value — easy to ignore with _ + Rust: .unwrap() can panic; ? propagates but hides the error + C++: exception might be caught too broadly + Move: abort is always propagated (safer by design) + +Q6.4: Is there a code path where NO return and NO error happens? + → Functions falling through without explicit return + → Default/zero values used when they shouldn't be + → Missing match/switch arms or else branches + → Language-specific: + Rust: compiler catches this. Go/C++: does not always. + Solidity: functions can fall through returning zero values. +``` + +### Category 7: External Call Reordering & Multi-Transaction State Analysis + +This category catches bugs that live in the TIMING and SEQUENCING of operations — both within a single transaction and across multiple transactions over time. + +#### Part A: External Call Reordering (within a single transaction) + +``` +Q7.1: If the function performs an external call BEFORE a state update, + what happens if I SWAP them — state update first, external call second? + → If the swap causes a revert: the ORIGINAL ordering may be exploitable + (the external call might re-enter or manipulate state before it's updated) + → If the swap works cleanly: the original ordering is likely safe, + OR the swap reveals the intended safe ordering was never enforced + → KEY: Try both directions. The one that reverts tells you which + ordering the code DEPENDS on. The one that doesn't revert tells you + which ordering an attacker can exploit. + +Q7.2: If the function performs an external call AFTER a state update, + what happens if I SWAP them — external call first, state update second? + → If the swap causes a revert: the current code is CORRECTLY ordered + (state must be updated before the external call can proceed) + → If the swap works cleanly: the ordering doesn't matter, OR + the external call could be exploited before state is finalized + → FINDING: If moving the external call BEFORE the state update + allows an attacker to observe/act on stale state, this is a bug. + +Q7.3: For EVERY external call in the function, ask: + "What can the CALLEE do with the current state at THIS exact moment?" + → At the point of the external call, what state is committed vs pending? + → Can the callee re-enter this contract/module and see inconsistent state? + → Can the callee call a DIFFERENT function that reads the not-yet-updated state? + → This applies beyond reentrancy: callbacks, hooks, oracle calls, + cross-contract reads — ANY outbound call is an opportunity for + the callee to act on intermediate state. + → Language examples: + Solidity: .call(), .transfer(), IERC20.safeTransfer(), callback hooks + Move: cross-module function calls during resource manipulation + Rust/Anchor: CPI (Cross-Program Invocation) in Solana + Go: outbound HTTP/RPC calls, goroutine spawning mid-operation + C++: virtual method calls, callback invocations, signal handlers + +Q7.4: What is the MINIMAL set of state that MUST be updated before each + external call to prevent exploitation? + → List every state variable the external callee could read or depend on + → If ANY of those variables are updated AFTER the external call, + flag it as a potential ordering vulnerability + → The fix is often: move the state update above the external call + (checks-effects-interactions pattern generalized to any language) +``` + +#### Part B: Multi-Transaction State Corruption (across time) + +``` +Q7.5: If a user calls this function with value X, and then calls it AGAIN + later with value Y — does the second call behave correctly given the + state changes from the first call? + → The first call changes state. Does the second call's logic ACCOUNT + for that changed state, or does it assume fresh/initial state? + → Example: deposit(100), then deposit(50). Does the second deposit + correctly handle shares/accounting when totalSupply is no longer 0? + → Example: borrow(1000), then borrow(500). Does the second borrow + check against the UPDATED debt, or does it re-read stale collateral? + +Q7.6: After transaction T1 changes state, does transaction T2 (same function, + different parameters) REVERT when it shouldn't, or SUCCEED when it shouldn't? + → Unexpected revert: T1's state change made a condition impossible for T2 + (e.g., T1 drains a pool below a minimum, T2 can't withdraw dust) + → Unexpected success: T1's state change should have blocked T2 but didn't + (e.g., T1 uses all collateral, T2 still borrows against phantom collateral) + → DEEP CHECK: Don't just test T2 immediately after T1. Test T2 after: + - Many T1s have accumulated (state drift over time) + - T1 with extreme values (max, min, dust) + - T1 from a different user (cross-user state pollution) + - T1 that was partially reverted (try-catch leaving dirty state) + +Q7.7: Does the accumulated state from MULTIPLE calls create a condition that + a SINGLE call can never reach? + → Rounding errors that compound: each call loses 1 wei of precision, + after 1000 calls the accounting is off by 1000 wei + → Monotonically growing state: counters, nonces, array lengths that + grow but never shrink — do they hit a ceiling or overflow? + → Reward/rate staleness: if updateReward() is called infrequently, + do accumulated rewards become incorrect? + → State fragmentation: many small operations leaving dust/remnants + that block future operations (e.g., can't close position because + of 1 wei of remaining debt) + + WORKED EXAMPLE — Partial Swap Fee Distribution Bug: + ───────────────────────────────────────────────────── + Consider an AMM pool with a swap() function that: + 1. Calculates amountOut based on reserves + 2. Updates accumulatedFees (used for LP fee distribution) + 3. Updates reserves + + Scenario — partial swap with wrong fee accounting: + State: reserveA=10000, reserveB=10000, accFees=0, totalLP=100 + + TX1: Alice swaps 1000 tokenA → tokenB (partial fill, 0.3% fee) + - fee = 3 tokenA → accFees updated to 3 + - BUT: the fee is added to accFees BEFORE reserves update + - reserveA becomes 11000, reserveB becomes ~9091 + - feePerLP = 3/100 = 0.03 per LP token ✓ (looks correct) + + TX2: Bob swaps 500 tokenA → tokenB (different amount, same function) + - fee = 1.5 tokenA → accFees updated to 4.5 + - BUT: feePerLP is now calculated as 4.5/100 = 0.045 + - The problem: the fee rate was computed using STALE reserve + ratios from before TX1 changed the pool composition + - After TX1, the pool is imbalanced — 1 tokenA is worth less + than before. But the fee accounting still values TX2's fee + at the OLD rate. + + TX3: Charlie claims LP fees + - Gets paid based on accFees = 4.5 at OLD token valuation + - But the pool's ACTUAL composition has shifted — the fees + are denominated in a token that's now worth less in the pool + - Result: fee distribution is skewed. Early LPs get overpaid, + late LPs get underpaid. Over hundreds of swaps, the + accounting diverges significantly from reality. + + The root cause: accFees is updated per-swap without rebasing + against the current reserve ratio. Each swap changes what "1 unit + of fee" is worth, but the accumulator treats all units as equal. + + → GENERALIZE THIS PATTERN to any system where: + - A global accumulator (fees, rewards, interest) is updated per-tx + - The VALUE of what's being accumulated changes between txs + - The accumulator doesn't rebase/normalize against current state + - Examples: LP fee distributors, staking reward accumulators, + interest rate models, rebasing token accounting, yield vaults + with variable share prices + + → CHECK SPECIFICALLY: + - Is the fee/reward denominated in a token whose relative value + changes with each operation? + - Does the accumulator use a snapshot of rates/prices that goes + stale after the state-changing operation? + - Are fees calculated BEFORE or AFTER the reserves/balances update? + (before = stale rate, after = correct rate, but BOTH must be checked) + - When multiple fee tiers or partial fills exist, does each partial + chunk use the UPDATED state from the previous chunk, or do they + all use the ORIGINAL state? (batch vs iterative accounting) + - After N swaps with varying sizes, does SUM(individual fees) equal + the fee you'd compute on the AGGREGATE swap? If not, the + accumulator is path-dependent and exploitable. + +Q7.8: Can an attacker craft a SEQUENCE of transactions to reach a state + that no single "normal" transaction path would produce? + → Deposit-borrow-withdraw-liquidate sequences that leave bad debt + → Stake-unstake-restake sequences that compound rounding errors + → Create-transfer-destroy sequences that orphan child state + → The attacker's advantage: they CHOOSE the order, amounts, and + timing. Test adversarial sequences, not just happy-path sequences. + → For each function, ask: "After calling THIS, what state is the + system in? What functions become newly available or newly dangerous + to call from that state?" +``` + +--- + +## Execution Process + +### Phase 0: Attacker Mindset (BEFORE reading a single line of code) + +``` +The bugs are in the answers to these 4 questions. +Ask them FIRST — they tell you WHERE to spend your time. + +Q0.1: What's the WORST thing an attacker can do here? + → Think attacker, NOT user. Users follow happy paths. + Attackers find the one path the dev never imagined. + → List the top 3-5 catastrophic outcomes: + drain all funds, brick the system, steal admin privileges, + manipulate prices/data, grief other users permanently, + corrupt state irreversibly, exfiltrate sensitive data. + → These become your ATTACK GOALS for the entire audit. + Every function you read, ask: "Does this help an + attacker achieve any of these goals?" + +Q0.2: What parts of the project are NOVEL? + → First-time code = first-time bugs. Period. + → Identify code that is NOT a fork/copy of battle-tested + libraries or frameworks. + Solidity: OpenZeppelin, Uniswap, Aave forks + Move: Aptos Framework, Sui Framework stdlib + Rust: well-known crates (tokio, serde, anchor) + Go: standard library, well-maintained packages + C++: STL, Boost, established frameworks + → Custom math, custom state machines, novel incentive + structures, unusual callback/hook patterns — + THIS is where your time pays off most. + → Standard library imports are unlikely to have bugs. + The glue code connecting them is where things break. + +Q0.3: Where does VALUE actually sit? + → Follow the money. Every expensive mistake involves + value moving somewhere it shouldn't. + → Map every module/component that holds: + - Funds (native tokens, coins, balances, account credits) + - Assets (tokens, NFTs, resources, inventory) + - Sensitive data (keys, credentials, PII) + - Accounting state (shares, debt, rewards, balances) + → For each value store, ask: "What code path moves + value OUT? What authorizes it? What validates the amount?" + → The functions touching these stores get 10x more scrutiny. + +Q0.4: What's the most COMPLEX interaction path? + → Complexity kills. The most complex path through the + system is the most likely to contain bugs. + → Map paths that: cross multiple modules/contracts/services, + involve callbacks or hooks, mix user input with external data, + have multiple branching conditions, or chain state changes. + → If a path touches 4+ modules or has 3+ external calls, + it's a prime candidate for state inconsistency bugs. + → Cross-module interaction + value movement = audit gold. +``` + +**Output of Phase 0:** A prioritized hit list. + +``` +┌─────────────────────────────────────────────────────┐ +│ PHASE 0 — ATTACKER'S HIT LIST │ +├─────────────────────────────────────────────────────┤ +│ │ +│ LANGUAGE: [detected language/framework] │ +│ │ +│ ATTACK GOALS (from Q0.1): │ +│ 1. [worst outcome] │ +│ 2. [second worst] │ +│ 3. [third worst] │ +│ │ +│ NOVEL CODE — highest bug density (from Q0.2): │ +│ - [module/file] — [why it's novel] │ +│ - [module/file] — [why it's novel] │ +│ │ +│ VALUE STORES — follow the money (from Q0.3): │ +│ - [module] holds [asset] — [outflow functions] │ +│ - [module] holds [asset] — [outflow functions] │ +│ │ +│ COMPLEX PATHS — complexity kills (from Q0.4): │ +│ - [path description] — [modules involved] │ +│ - [path description] — [modules involved] │ +│ │ +│ PRIORITY ORDER (spend time here first): │ +│ 1. [highest priority target + why] │ +│ 2. [second priority target + why] │ +│ 3. [third priority target + why] │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +Functions and modules that appear in MULTIPLE answers above get audited FIRST +and with the DEEPEST scrutiny in Phase 2. Everything else is secondary. + +--- + +### Phase 1: Scope & Inventory + +``` +1. Identify ALL modules/contracts/packages in scope +2. For each module, list: + - ALL entry points (public/exported/external functions — the attack surface) + - ALL state they read/write (storage, globals, struct fields, DB) + - ALL access guards applied (modifiers, auth checks, visibility) + - ALL internal functions they call +3. Build a FUNCTION-STATE MATRIX: + | Function | Reads | Writes | Guards | Calls | + |----------|-------|--------|--------|-------| + This matrix is your map for consistency analysis (Category 3) +``` + +### Phase 2: Individual Function Deep Dive + +For EACH function, perform the Feynman interrogation: + +``` +┌─────────────────────────────────────────────────────┐ +│ FUNCTION: [module.functionName] │ +│ Visibility: [public/private/internal/exported] │ +│ Guards: [access control, auth checks, decorators] │ +│ State reads: [variables/fields/storage] │ +│ State writes: [variables/fields/storage] │ +│ External calls: [targets] │ +├─────────────────────────────────────────────────────┤ +│ │ +│ LINE-BY-LINE INTERROGATION: │ +│ │ +│ L[N]: [code line] │ +│ Q1.1 → WHY: [explanation or "CANNOT EXPLAIN" flag] │ +│ Q2.1 → ORDER: [what if moved up?] │ +│ Q2.2 → ORDER: [what if moved down?] │ +│ Q4.x → ASSUMES: [hidden assumption found] │ +│ Q5.x → EDGE: [boundary behavior] │ +│ → VERDICT: SOUND | SUSPECT | VULNERABLE │ +│ → If SUSPECT/VULNERABLE: [specific scenario] │ +│ │ +│ CROSS-FUNCTION CHECK: │ +│ Q3.1 → [guard consistency with sibling functions] │ +│ Q3.2 → [inverse operation parity] │ +│ Q3.3 → [parameter validation consistency] │ +│ │ +│ FUNCTION VERDICT: SOUND | HAS_CONCERNS | VULNERABLE │ +└─────────────────────────────────────────────────────┘ +``` + +**IMPORTANT**: You do NOT need to ask ALL questions for ALL lines. Use judgment: +- State-changing lines → heavy on Q2 (ordering) and Q4 (assumptions) +- Validation/guard lines → heavy on Q1 (purpose) and Q3 (consistency) +- External calls / cross-module calls → heavy on Q4 (assumptions), Q5 (edges), Q6 (returns) +- Math operations → heavy on Q5 (boundaries) and Q4.6 (amount assumptions) + +### Phase 3: Cross-Function Analysis + +``` +Using the Function-State Matrix from Phase 1: + +1. GUARD CONSISTENCY + - Group functions by the state variables they WRITE + - Within each group, list all access guards + - FLAG: Any function missing a guard its siblings have + +2. INVERSE OPERATION PARITY + - Pair up: deposit/withdraw, mint/burn, stake/unstake, + create/destroy, add/remove, open/close, encode/decode, etc. + - For each pair, compare: + - Parameter validation (Q3.2) + - State changes (are they truly inverse?) + - Access control (should both require same auth?) + - Event/log emission (are both tracked?) + +3. STATE TRANSITION INTEGRITY + - Map all valid state transitions + - For each transition, verify: + - Can it be triggered out of expected order? + - Can it be skipped entirely? + - Can it be triggered by an unauthorized actor? + - What if it's triggered when the system is in an unexpected state? + +4. VALUE FLOW TRACKING + - Trace value/asset flows across function boundaries + - Verify: value in == value out (conservation) + - FLAG: Any path where value can be created or destroyed unexpectedly +``` + +### Phase 4: Synthesize Raw Findings + +``` +For each SUSPECT or VULNERABLE verdict: + +1. Write the QUESTION that exposed it +2. Describe the SCENARIO (step-by-step) +3. Show the AFFECTED CODE (exact lines) +4. Explain WHY the current code fails +5. Assess IMPACT (what can an attacker gain/break?) +6. Classify severity: CRITICAL / HIGH / MEDIUM / LOW +7. Suggest a FIX (minimal, targeted) +``` + +Save raw (unverified) findings to: `.audit/findings/feynman-analysis-raw.md` + +**IMPORTANT: Do NOT report raw findings to the user as final results.** +These are HYPOTHESES that must be verified in Phase 5 before inclusion in the final report. + +--- + +### Phase 5: Verification Gate (MANDATORY before final report) + +**Every CRITICAL, HIGH, and MEDIUM finding from Phase 4 MUST be verified before +being included in the final report.** Feynman reasoning surfaces many hypotheses, +but code-level reasoning alone produces false positives (wrong mechanism assumed, +mitigating code missed, incorrect severity assessment). Verification eliminates +these before they reach the user. + +``` +VERIFICATION RULE: No C/H/M finding goes into the final report unverified. +Raw findings are HYPOTHESES. Verified findings are RESULTS. +``` + +#### Verification Methods (use whichever is most appropriate per finding): + +**Method A: Deep Code Trace Verification** +For findings about missing checks, wrong parameters, or inconsistent validation: +1. Read the EXACT lines cited in the finding +2. Trace the complete call chain (caller → callee → downstream effects) +3. Check for mitigating code elsewhere (guards in called functions, validation in callers) +4. Confirm the scenario is reachable end-to-end +5. Verdict: TRUE POSITIVE / FALSE POSITIVE / DOWNGRADE + +**Method B: PoC Test Verification** +For findings about math errors, rounding drift, resource limits, or state accounting: +1. Write a test using the project's native test framework: + - Solidity: Foundry test → `forge test --match-path "test/audit/[file]" -vvv` + - Move: `aptos move test --filter [test_name]` or `sui move test` + - Rust: `cargo test [test_name] -- --nocapture` + - Go: `go test -run TestName -v` + - C++: gtest/catch2 equivalent +2. The PoC must demonstrate the EXACT scenario described in the finding +3. If the test passes and output confirms the issue: TRUE POSITIVE +4. If the test fails or output disproves the claim: FALSE POSITIVE or DOWNGRADE + +**Method C: Hybrid (Code Trace + PoC)** +For complex findings spanning multiple modules: +1. First do a code trace to confirm the mechanism is plausible +2. Then write a PoC to confirm with concrete values and runtime behavior + +#### What to verify for each severity: + +| Severity | Verification Required | Method | +|----------|----------------------|--------| +| CRITICAL | MANDATORY — PoC required (Method B or C) | Must demonstrate value loss or permanent DoS with concrete numbers | +| HIGH | MANDATORY — Code trace + PoC recommended (Method A or C) | Must confirm the broken invariant is reachable | +| MEDIUM | MANDATORY — Code trace minimum (Method A) | Must confirm the mechanism is correct and not mitigated elsewhere | +| LOW | Optional — Code inspection sufficient | Quick sanity check: is the line/function real? | + +#### Verification Checklist (per finding): + +``` +[] 1. Does the cited code actually exist at the stated line numbers? +[] 2. Is the described mechanism correct? (trace the actual math/logic) +[] 3. Are there mitigating factors the finding missed? + - Called functions that add validation + - Access guards on calling functions + - Upstream checks that prevent the scenario + - Downstream checks that catch the error + - Language-level safety (borrow checker, type system, Move verifier) +[] 4. Is the severity accurate given the ACTUAL impact? + - Does "value loss" actually mean "revert/abort with confusing error"? + - Does "permanent DoS" actually mean "self-griefing only"? + - Is the "missing check" actually handled by a different code path? +[] 5. For PoC-verified findings: does the test output match the claim? +``` + +#### Common False Positive Patterns from Feynman Analysis: + +These patterns frequently produce hypotheses that fail verification: + +1. **"Missing authorization" that exists in a different layer:** + Finding says auth is missing, but the caller/router/middleware already + enforces it before this function is reachable. + +2. **"Rounding drift" that's cleaned by downstream code:** + Finding identifies `scale_up(scale_down(x)) < x` but misses cleanup + applied upstream that ensures x is always a clean multiple. + +3. **"No validation" that errors downstream:** + Finding says a parameter isn't validated, but the called function has its own + validation that catches invalid inputs (just with a confusing error message). + +4. **"Unbounded loop" bounded by design or economics:** + Finding says a loop has no cap, but the data structure is bounded by design, + or the economic cost of creating the DoS condition exceeds the benefit. + +5. **"Severity inflation":** + Finding claims CRITICAL (value loss) but actual impact is MEDIUM (error/DoS) + because a safety check catches the issue before value is affected. + +6. **"Language safety ignored":** + Finding claims overflow/underflow but the language aborts on overflow by + default (Move, Rust in debug, Solidity >=0.8). Or finding claims memory + unsafety in a memory-safe language. + +#### Phase 5 Output: + +After verification, produce the VERIFIED findings file: + +Save to: `.audit/findings/feynman-verified.md` + +```markdown +# Feynman Audit — Verified Findings + +## Verification Summary +| ID | Original Severity | Verdict | Final Severity | +|----|-------------------|---------|----------------| +| FF-001 | CRITICAL | TRUE POSITIVE — DOWNGRADE | LOW | +| FF-002 | HIGH | TRUE POSITIVE | HIGH | +| FF-003 | MEDIUM | FALSE POSITIVE | — | +| ... | ... | ... | ... | + +## Verified TRUE POSITIVE Findings +[Only findings that passed verification, with final severity] + +## False Positives Eliminated +[Findings that failed verification, with explanation of why] + +## Downgraded Findings +[Findings where severity was reduced, with justification] +``` + +**Only the verified findings file should be presented to the user as the final report.** + +--- + +## Severity Classification + +| Severity | Criteria | +|----------|----------| +| **CRITICAL** | Direct value/fund loss, permanent DoS, or system insolvency | +| **HIGH** | Conditional value loss, privilege escalation, or broken core invariant | +| **MEDIUM** | Value leakage, griefing with cost, or degraded functionality | +| **LOW** | Informational, inefficiency, or cosmetic inconsistency with no exploit | + +--- + +## Output Format + +Two files are produced during the audit: + +### 1. Raw Findings (intermediate — NOT the final deliverable) + +Save to: `.audit/findings/feynman-analysis-raw.md` + +This contains ALL hypotheses from Phases 1-4 before verification. Include the +Function-State Matrix, Guard Consistency Analysis, Inverse Operation Parity, +and all raw findings with their initial severity classification. + +### 2. Verified Findings (FINAL deliverable — present this to the user) + +Save to: `.audit/findings/feynman-verified.md` + +```markdown +# Feynman Audit — Verified Findings + +## Scope +- Language: [detected language] +- Modules analyzed: [list] +- Functions analyzed: [count] +- Lines interrogated: [count] + +## Verification Summary +| ID | Original Severity | Verdict | Final Severity | +|----|-------------------|---------|----------------| + +## Function-State Matrix +[The matrix from Phase 1] + +## Guard Consistency Analysis +[Results from Phase 3.1 — which functions are missing expected guards] + +## Inverse Operation Parity +[Results from Phase 3.2 — asymmetries between paired operations] + +## Verified Findings (TRUE POSITIVES only) + +### Finding FF-001: [Title] +**Severity:** CRITICAL | HIGH | MEDIUM | LOW +**Module:** [name] +**Function:** [name] +**Lines:** [L:start-end] +**Verification:** [Code trace / PoC / Hybrid] — [test file if PoC] + +**Feynman Question that exposed this:** +> [The exact question from the framework] + +**The code:** +```[language] +// [affected code block] +``` + +**Why this is wrong:** +[First-principles explanation — no jargon, no pattern names. +Explain like you're teaching someone who has never seen this bug class.] + +**Verification evidence:** +[For code trace: the exact mitigating/confirming code paths traced] +[For PoC: test name, key log output, concrete numbers] + +**Attack scenario:** +1. [Step-by-step exploitation] + +**Impact:** +[What an attacker gains or what breaks] + +**Suggested fix:** +```[language] +// [minimal fix] +``` + +--- + +## False Positives Eliminated +[Findings that failed verification, with explanation of WHY they are false] + +## Downgraded Findings +[Findings where severity was reduced, with justification] + +## LOW Findings (verified by inspection) +[Table of LOW findings with brief verdict] + +## Summary +- Total functions analyzed: [N] +- Raw findings (pre-verification): [N] CRITICAL | [N] HIGH | [N] MEDIUM | [N] LOW +- After verification: [N] TRUE POSITIVE | [N] FALSE POSITIVE | [N] DOWNGRADED +- Final: [N] HIGH | [N] MEDIUM | [N] LOW +``` + +--- + +## Post-Audit Actions + +| Scenario | Action | +|----------|--------| +| Need deeper context on a function | Re-read the function and its callers line-by-line | +| Finding confirmed as true positive | Write up with severity, trigger sequence, PoC, and fix | +| Need exploit validation | Write a Foundry/Hardhat PoC test to confirm | +| Uncertain about design intent | Check NatSpec, comments, and project documentation | + +--- + +## Anti-Hallucination Protocol + +``` +NEVER: +- Invent code that doesn't exist in the codebase +- Assume a function has an access guard without verifying +- Claim a variable is uninitialized without checking constructors/initializers +- Report a finding without showing the exact vulnerable code +- Use phrases like "could potentially" or "might be vulnerable" +- Apply language-specific assumptions to a different language + (e.g., don't assume Rust code has Solidity's reentrancy model) + +ALWAYS: +- Read the actual code before questioning it +- Verify your assumptions by reading called functions +- Check constructors, initializers, and default values +- Confirm guard/access-control behavior by reading the actual implementation +- Show exact file paths and line numbers for all references +- Use the correct language terminology (not Solidity terms for Rust code) +``` + +--- + +## Quick-Start Checklist + +When starting a Feynman audit: + +- [ ] **Phase 0:** Detect the language and adapt terminology +- [ ] **Phase 0:** Answer the 4 attacker mindset questions BEFORE reading code +- [ ] **Phase 0:** Build the Attacker's Hit List (attack goals, novel code, value stores, complex paths) +- [ ] **Phase 0:** Prioritize targets — functions appearing in multiple answers get audited first +- [ ] **Phase 1:** List all modules/contracts/packages in scope +- [ ] **Phase 1:** Build the Function-State Matrix +- [ ] **Phase 1:** Identify all function pairs (deposit/withdraw, etc.) +- [ ] **Phase 2:** Run Feynman interrogation on every entry point (priority order from Phase 0) +- [ ] **Phase 3:** Run cross-function analysis (guard consistency, inverse parity, value flow) +- [ ] **Phase 4:** Document all SUSPECT and VULNERABLE verdicts as raw findings +- [ ] **Phase 4:** Save raw findings to `.audit/findings/feynman-analysis-raw.md` +- [ ] **Phase 5:** Verify ALL C/H/M findings (code trace + PoC where needed) +- [ ] **Phase 5:** Eliminate false positives, downgrade inflated severities +- [ ] **Phase 5:** Save verified findings to `.audit/findings/feynman-verified.md` +- [ ] **Phase 5:** Present ONLY the verified report to the user diff --git a/.claude/skills/nemesis-auditor/SKILL.md b/.claude/skills/nemesis-auditor/SKILL.md new file mode 100644 index 00000000..978d0233 --- /dev/null +++ b/.claude/skills/nemesis-auditor/SKILL.md @@ -0,0 +1,1047 @@ +--- +name: nemesis-auditor +description: "The Inescapable Auditor. Runs the full Feynman Auditor (Stage 1) and full State Inconsistency Auditor (Stage 2) as primary steps, then fuses their outputs in a feedback loop (Stage 3) to find bugs at the intersection that neither alone would catch. Language-agnostic. Triggers on /nemesis or nemesis audit." +--- + +# N E M E S I S +### The Inescapable Auditor + +``` + ╔═══════════════════════════════════════════════════════════════╗ + ║ ║ + ║ "Nemesis — the goddess of divine retribution against ║ + ║ those who succumb to hubris." ║ + ║ ║ + ║ Your code was written with confidence. ║ + ║ Nemesis questions that confidence. ║ + ║ Then maps what your confidence forgot to protect. ║ + ║ Then questions it again. ║ + ║ ║ + ║ Nothing survives both passes. ║ + ║ ║ + ╚═══════════════════════════════════════════════════════════════╝ +``` + +Not three sequential stages. An **iterative back-and-forth loop** where Feynman and State Inconsistency run alternating passes — each pass informed by the previous pass's findings — until no new bugs surface. + +**Pass 1 (Feynman)** — Run the **complete Feynman Auditor** (`.claude/skills/feynman-auditor/SKILL.md`). Every line questioned. Every ordering challenged. Every assumption exposed. Collect findings + suspects. + +**Pass 2 (State)** — Run the **complete State Inconsistency Auditor** (`.claude/skills/state-inconsistency-auditor/SKILL.md`), **enriched by Pass 1's findings**. Feynman suspects become extra audit targets. Feynman's exposed assumptions reveal new coupled pairs to map. Collect findings + gaps. + +**Pass 3 (Feynman)** — Re-run Feynman **only on functions/state touched by Pass 2's new findings**. State gaps become new Feynman interrogation targets. Ask: "WHY is this sync missing? What assumption led to the gap? What breaks downstream?" Collect new findings. + +**Pass 4 (State)** — Re-run State Mapper **only on new coupled pairs and mutation paths exposed by Pass 3**. Check if Feynman's new findings reveal additional state desync. Collect new findings. + +**...continue alternating until convergence (no new findings in a pass).** + +**Language-agnostic.** Works on Solidity, Move, Rust, Go, C++, or anything else. + +--- + +## When to Activate + +- User says `/nemesis` or `nemesis audit` or `deep combined audit` +- User wants maximum-depth business logic + state inconsistency coverage +- When the codebase is complex enough that either auditor alone would miss cross-cutting bugs + +## When NOT to Use + +- Quick pattern-matching scans where you only need known vulnerability patterns +- Simple spec compliance checks +- Report generation from existing findings + +--- + +## The Nemesis Execution Model: Iterative Back-and-Forth + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ N E M E S I S — I T E R A T I V E L O O P │ +├──────────────────────────────────────────────────────────────────────┤ +│ │ +│ PHASE 0: RECON │ +│ ─────────────── │ +│ Attacker mindset (Q0.1-Q0.5) + Initial coupling hypothesis │ +│ Output: Hit List + Priority Targets │ +│ │ +│ ════════════════════════════════════════════════════════════════ │ +│ ║ ITERATIVE PASS LOOP BEGINS ║ │ +│ ════════════════════════════════════════════════════════════════ │ +│ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ PASS 1 — FEYNMAN (full skill, first run) │ │ +│ │ │ │ +│ │ Load: .claude/skills/feynman-auditor/SKILL.md │ │ +│ │ Execute complete pipeline: Phase 0→1→2→3→4→5 │ │ +│ │ Input: Raw codebase + Phase 0 hit list │ │ +│ │ Output: │ │ +│ │ • Verified findings (.audit/findings/feynman-pass1.md) │ │ +│ │ • SUSPECT verdicts (functions + state vars flagged) │ │ +│ │ • Exposed assumptions (implicit trusts about state) │ │ +│ │ • Ordering concerns (external call timing issues) │ │ +│ │ • Multi-tx state corruption candidates │ │ +│ │ • Function-State Matrix │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ ↓ feed forward │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ PASS 2 — STATE INCONSISTENCY (full skill, enriched) │ │ +│ │ │ │ +│ │ Load: .claude/skills/state-inconsistency-auditor/SKILL.md │ │ +│ │ Execute complete pipeline: Phase 1→2→3→4→5→6→7→8 │ │ +│ │ Input: Raw codebase + ALL of Pass 1's output │ │ +│ │ │ │ +│ │ ENRICHMENT from Pass 1: │ │ +│ │ • Feynman SUSPECTS → add as extra state audit targets │ │ +│ │ • Exposed assumptions → reveal NEW coupled pairs │ │ +│ │ ("dev assumes X stays in sync" → map X as coupled) │ │ +│ │ • Ordering concerns → check if state gap exists at │ │ +│ │ the flagged ordering point │ │ +│ │ • Function-State Matrix → use as base for Mutation Matrix │ │ +│ │ │ │ +│ │ Output: │ │ +│ │ • Verified findings (.audit/findings/state-pass2.md) │ │ +│ │ • State GAPS (functions missing coupled updates) │ │ +│ │ • New coupled pairs discovered via Feynman enrichment │ │ +│ │ • Masking code flagged (ternary clamps, min caps) │ │ +│ │ • Parallel path mismatches │ │ +│ │ • Coupled State Dependency Map │ │ +│ │ • Mutation Matrix │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ ↓ feed back │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ PASS 3 — FEYNMAN RE-INTERROGATION (targeted, not full) │ │ +│ │ │ │ +│ │ Scope: ONLY functions/state touched by Pass 2's NEW output │ │ +│ │ DO NOT re-audit what Pass 1 already cleared. │ │ +│ │ │ │ +│ │ For each State GAP from Pass 2: │ │ +│ │ Q: "WHY doesn't [function] update [coupled state B]?" │ │ +│ │ Q: "What ASSUMPTION led to this gap?" │ │ +│ │ Q: "What DOWNSTREAM function reads B and breaks?" │ │ +│ │ Q: "Can an attacker CHOOSE a sequence to exploit this?" │ │ +│ │ │ │ +│ │ For each MASKING CODE from Pass 2: │ │ +│ │ Q: "WHY would this ever underflow/overflow?" │ │ +│ │ Q: "What invariant is ACTUALLY broken underneath?" │ │ +│ │ → Trace the broken invariant to its root cause mutation │ │ +│ │ │ │ +│ │ For each NEW COUPLED PAIR from Pass 2: │ │ +│ │ Q: "Is this coupling intentional or accidental?" │ │ +│ │ Q: "What ordering constraints exist between the pair?" │ │ +│ │ Q: "What happens across multiple txs as both drift?" │ │ +│ │ │ │ +│ │ Output: │ │ +│ │ • New findings (.audit/findings/feynman-pass3.md) │ │ +│ │ • New suspects (if any) │ │ +│ │ • Deeper root cause analysis on Pass 2 gaps │ │ +│ │ • Multi-tx adversarial sequences for confirmed bugs │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ ↓ feed back │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ PASS 4 — STATE RE-ANALYSIS (targeted, not full) │ │ +│ │ │ │ +│ │ Scope: ONLY new coupled pairs + mutation paths from Pass 3 │ │ +│ │ DO NOT re-audit what Pass 2 already cleared. │ │ +│ │ │ │ +│ │ For each NEW SUSPECT from Pass 3: │ │ +│ │ → Is this suspect state part of a coupled pair? │ │ +│ │ → Does the suspect function update all counterparts? │ │ +│ │ → Does the root cause analysis reveal additional gaps? │ │ +│ │ │ │ +│ │ For each ROOT CAUSE from Pass 3: │ │ +│ │ → Trace the root cause mutation through ALL code paths │ │ +│ │ → Check parallel paths for the same root cause │ │ +│ │ → Check if the root cause affects other coupled pairs │ │ +│ │ │ │ +│ │ Output: │ │ +│ │ • New findings (.audit/findings/state-pass4.md) │ │ +│ │ • Any remaining gaps or suspects │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ CONVERGENCE CHECK │ │ +│ │ │ │ +│ │ Did the last pass produce ANY new: │ │ +│ │ - Findings not in previous passes? │ │ +│ │ - Coupled pairs not previously mapped? │ │ +│ │ - Suspects not previously flagged? │ │ +│ │ - Root causes not previously traced? │ │ +│ │ │ │ +│ │ IF YES → Continue: Run Pass N+1 (alternate Feynman/State) │ │ +│ │ Scope: ONLY new items from the previous pass │ │ +│ │ │ │ +│ │ IF NO → Converged. Proceed to Final Phase. │ │ +│ │ │ │ +│ │ SAFETY: Maximum 6 total passes (3 Feynman + 3 State) │ │ +│ │ to prevent infinite loops. │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +│ ════════════════════════════════════════════════════════════════ │ +│ ║ ITERATIVE LOOP ENDS ║ │ +│ ════════════════════════════════════════════════════════════════ │ +│ │ +│ FINAL PHASE: CONSOLIDATION │ +│ ─────────────────────────── │ +│ 1. Merge all pass outputs into unified finding set │ +│ 2. Deduplicate (same root cause found from both sides) │ +│ 3. Multi-Tx adversarial sequence tracing on ALL confirmed bugs │ +│ 4. Final Verification Gate (code trace + PoC for all C/H/M) │ +│ 5. Tag each finding with discovery path: │ +│ • "Feynman-only" — found in Pass 1, never enriched by State │ +│ • "State-only" — found in Pass 2, never enriched by Feynman │ +│ • "Cross-feed P[N]→P[M]" — found via back-and-forth interaction │ +│ 6. Output: .audit/findings/nemesis-verified.md │ +│ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +**KEY RULES FOR THE ITERATIVE LOOP:** + +``` +1. Pass 1 (Feynman) and Pass 2 (State) are FULL skill runs — complete pipelines. + They establish the baseline. + +2. Pass 3+ are TARGETED — only audit new items surfaced by the previous pass. + Do NOT re-audit what was already cleared. This prevents redundant work + while ensuring every new discovery gets deep analysis from both perspectives. + +3. Each pass MUST produce a delta — what's NEW compared to all previous passes. + The delta is what feeds the next pass. No delta = convergence. + +4. Alternate strictly: Feynman → State → Feynman → State → ... + Never run the same auditor twice in a row. + +5. Maximum 6 total passes (3 Feynman + 3 State). In practice, most audits + converge in 3-4 passes (Pass 1 + Pass 2 + 1-2 targeted re-passes). + +6. Track the DISCOVERY PATH for every finding. Findings that emerged from + cross-feed (e.g., "State gap in Pass 2 → Feynman root cause in Pass 3") + are the highest-value discoveries — they prove the loop's worth. +``` + +--- + +## Core Philosophy + +``` +Feynman alone finds logic bugs but may miss state coupling gaps. +State Mapper alone finds desync bugs but may miss WHY the state was designed that way. + +NEMESIS runs them BACK AND FORTH — each pass feeds the next. + +The iterative loop: +┌─────────────────────────────────────────────────────────────┐ +│ │ +│ PASS 1 — FEYNMAN (full run): │ +│ "WHY does this state update exist?" │ +│ → Finds: ordering bugs, assumption violations, suspects │ +│ → Exposes: "This line maintains invariant X with State B" │ +│ │ +│ ↓ feed suspects + assumptions + matrix forward │ +│ │ +│ PASS 2 — STATE (full run, enriched by Pass 1): │ +│ "Do ALL paths that touch A also touch B?" │ +│ → Uses Feynman suspects as extra audit targets │ +│ → Uses exposed assumptions to discover NEW coupled pairs │ +│ → Finds: gaps, masking code, parallel path mismatches │ +│ │ +│ ↓ feed gaps + masking code + new pairs back │ +│ │ +│ PASS 3 — FEYNMAN (targeted re-interrogation): │ +│ "WHY doesn't liquidate() update B?" │ +│ "What assumption led to this gap?" │ +│ "What breaks downstream after N transactions?" │ +│ → Root cause analysis on State's gaps │ +│ → NEW suspects emerge from deeper questioning │ +│ │ +│ ↓ feed new suspects + root causes back │ +│ │ +│ PASS 4 — STATE (targeted re-analysis): │ +│ "Does this root cause affect OTHER coupled pairs?" │ +│ "Do parallel paths share the same root cause?" │ +│ → Finds ADDITIONAL gaps via root cause propagation │ +│ │ +│ ↓ ... continue until convergence ... │ +│ │ +│ CONVERGED — No new findings in the last pass. │ +│ Consolidate + verify + deliver. │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Core Rules + +``` +RULE 0: THE ITERATIVE LOOP IS MANDATORY +Never run Feynman and State Mapper as isolated one-shot passes. +They MUST alternate back and forth. Each pass feeds the next. +The loop runs until no new findings emerge. + +RULE 1: FULL FIRST, TARGETED AFTER +Pass 1 (Feynman) and Pass 2 (State) are FULL skill runs. +Pass 3+ are TARGETED — only audit the delta from the previous pass. +Never re-audit what was already cleared. Always go deeper on what's new. + +RULE 2: EVERY COUPLED PAIR GETS INTERROGATED +The State Mapper finds pairs. Feynman interrogates each one: +"Why are these coupled? What invariant links them? Is the +invariant ACTUALLY maintained by every mutation path?" + +RULE 3: EVERY FEYNMAN SUSPECT GETS STATE-TRACED +When Feynman flags a line as SUSPECT, the State Mapper traces +every state variable that line touches, maps all their coupled +dependencies, and checks if the suspicion propagates. + +RULE 4: PARTIAL OPERATIONS + ORDERING = GOLD +The intersection of "partial state change" (State Mapper's +specialty) and "operation ordering" (Feynman's Category 2 & 7) +is where the highest-value bugs live. + +RULE 5: DEFENSIVE CODE IS A SIGNAL, NOT A SOLUTION +When the State Mapper finds masking code (ternary clamps, min caps), +Feynman interrogates WHY it exists. The mask reveals the invariant +that's actually broken underneath. + +RULE 6: EVIDENCE OR SILENCE +No finding without: coupled pair, breaking operation, trigger +sequence, downstream consequence, and verification. +``` + +--- + +## Language Adaptation + +Detect the language and adapt. The questions and methodology are universal. + +| Concept | Solidity | Move | Rust | Go | C++ | +|---------|----------|------|------|----|-----| +| Module/unit | contract | module | crate/mod | package | class/namespace | +| Entry point | external/public fn | public fun | pub fn | Exported fn | public method | +| State storage | storage variables | global storage / resources | struct fields / state | struct fields / DB | member variables | +| Access guard | modifier | access control / friend | trait bound / #[cfg] | middleware / auth | access specifier | +| Mapping | mapping(k => v) | Table\ | HashMap / BTreeMap | map[K]V | std::map | +| Delete | delete mapping[key] | table::remove | map.remove(&key) | delete(map, key) | map.erase(key) | +| Caller identity | msg.sender | &signer | caller / Context | ctx / request.User | this / session | +| Error/abort | revert / require | abort / assert! | panic! / Result::Err | error / panic | throw / exception | +| Checked math | 0.8+ auto / SafeMath | built-in overflow abort | checked_add | math/big | safe int libs | +| External call | .call() / interface | cross-module call | CPI (Solana) | RPC / HTTP | virtual call | +| Test framework | Foundry / Hardhat | Move Prover / aptos test | cargo test | go test | gtest / catch2 | + +--- + +## The Nemesis Execution Pipeline + +``` +┌───────────────────────────────────────────────────────────────────┐ +│ N E M E S I S P I P E L I N E │ +├───────────────────────────────────────────────────────────────────┤ +│ │ +│ RECON Phase 0: Attacker Mindset + Hit List │ +│ ───── (Feynman Q0.1-Q0.4 + State value store mapping) │ +│ │ +│ FOUNDATION Phase 1: Dual Mapping │ +│ ────────── ├─ Feynman: Function-State Matrix │ +│ └─ State: Coupled State Dependency Map │ +│ │ +│ HUNT PASS 1 Phase 2: Feynman Interrogation (all 7 categories) │ +│ ─────────── Each SUSPECT verdict → fed to Phase 3 │ +│ │ +│ HUNT PASS 2 Phase 3: State Cross-Check │ +│ ─────────── Mutation Matrix + Parallel Path Comparison │ +│ + Feynman suspects as extra audit targets │ +│ │ +│ FEEDBACK Phase 4: The Nemesis Loop │ +│ ──────── ├─ State gaps → Feynman re-interrogation │ +│ ├─ Feynman findings → State dependency expansion │ +│ ├─ Masking code → Feynman "WHY" questioning │ +│ └─ Loop until convergence (no new findings) │ +│ │ +│ SEQUENCES Phase 5: Multi-Transaction Journey Tracing │ +│ ───────── Adversarial sequences across both dimensions │ +│ │ +│ VERIFY Phase 6: Verification Gate │ +│ ────── Code trace + PoC for all C/H/M findings │ +│ │ +│ DELIVER Phase 7: Final Report │ +│ ─────── Only TRUE POSITIVES. Zero noise. │ +│ │ +└───────────────────────────────────────────────────────────────────┘ +``` + +--- + +### Phase 0: Attacker Recon (BEFORE reading code) + +Combine Feynman's attacker mindset with State Mapper's value tracking: + +``` +Q0.1: ATTACK GOALS — What's the WORST an attacker can achieve? + List top 3-5 catastrophic outcomes. These drive the entire audit. + +Q0.2: NOVEL CODE — What's NOT a fork of battle-tested code? + Custom math, novel mechanisms, unique state machines = highest bug density. + +Q0.3: VALUE STORES — Where does value actually sit? + Map every module that holds funds, assets, accounting state. + For each: what code path moves value OUT? What authorizes it? + +Q0.4: COMPLEX PATHS — What's the most complex interaction path? + Paths crossing 4+ modules with 3+ external calls = prime targets. + +Q0.5: COUPLED VALUE — Which value stores have DEPENDENT accounting? + (NEW — State Mapper contribution to recon) + For each value store from Q0.3, ask: "What other storage must + stay in sync with this?" Build the initial coupling hypothesis + BEFORE reading code. The code will confirm or reveal more. +``` + +**Output:** Attacker's Hit List + Initial Coupling Hypothesis + +``` +┌─────────────────────────────────────────────────────────────┐ +│ PHASE 0 — NEMESIS RECON │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ LANGUAGE: [detected] │ +│ │ +│ ATTACK GOALS: │ +│ 1. [worst outcome] │ +│ 2. [second worst] │ +│ 3. [third worst] │ +│ │ +│ NOVEL CODE (highest bug density): │ +│ - [module] — [why novel] │ +│ │ +│ VALUE STORES + INITIAL COUPLING HYPOTHESIS: │ +│ - [module] holds [asset] │ +│ Outflows: [functions] │ +│ Suspected coupled state: [what must sync] │ +│ │ +│ COMPLEX PATHS: │ +│ - [path] — [modules involved] │ +│ │ +│ PRIORITY ORDER: │ +│ 1. [target] — appears in [N] answers above │ +│ 2. [target] — appears in [N] answers above │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +### Phase 1: Dual Mapping (Foundation for both auditors) + +Run both mapping operations simultaneously. They share the same codebase scan. + +#### 1A: Function-State Matrix (Feynman foundation) + +``` +For each module, list: +- ALL entry points (public/exported/external functions) +- ALL state they read/write +- ALL access guards applied +- ALL internal functions they call +- ALL external calls they make + +| Function | Reads | Writes | Guards | Internal Calls | External Calls | +|----------|-------|--------|--------|----------------|----------------| +``` + +#### 1B: Coupled State Dependency Map (State Mapper foundation) + +``` +For every storage variable, ask: +"What other storage values MUST change when this one changes?" + +Build the dependency graph: + State A changes → State B MUST change (invariant: [relationship]) + State C changes → State D AND State E MUST change + +Look for: +- per-user balance ↔ per-user accumulator/tracker/checkpoint +- numerator ↔ denominator +- position size ↔ position-derived values (health, rewards, shares) +- total/aggregate ↔ sum of individual components +- any cached computation ↔ inputs it was derived from +- any index/accumulator ↔ last-snapshot of that index per user +``` + +#### 1C: Cross-Reference (THE NEMESIS DIFFERENCE) + +``` +Overlay the two maps: + +For each COUPLED PAIR from 1B: + → Find ALL functions from 1A that WRITE to either side + → Mark which functions update BOTH sides vs only ONE side + → Functions that update only ONE side = PRIMARY AUDIT TARGETS + +For each FUNCTION from 1A: + → List ALL state variables it writes + → For each written variable, check 1B: is it part of a coupled pair? + → If yes: does this function ALSO write the coupled counterpart? + → If no: mark as STATE GAP — feed to Phase 3 AND Phase 4 +``` + +**Output:** Unified Nemesis Map — functions × state × couplings × gaps + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ NEMESIS MAP — Phase 1 Cross-Reference │ +├───────────────┬──────────┬──────────┬──────────┬──────────────────┤ +│ Function │ Writes A │ Writes B │ A↔B Pair │ Sync Status │ +├───────────────┼──────────┼──────────┼──────────┼──────────────────┤ +│ deposit() │ ✓ │ ✓ │ bal↔chk │ ✓ SYNCED │ +│ withdraw() │ ✓ │ ✓ │ bal↔chk │ ✓ SYNCED │ +│ transfer() │ ✓ │ ✗ │ bal↔chk │ ✗ GAP → Phase 4 │ +│ liquidate() │ ✓ │ ✗ │ bal↔chk │ ✗ GAP → Phase 4 │ +│ emergencyW() │ ✓ │ ✗ │ bal↔chk │ ✗ GAP → Phase 4 │ +└───────────────┴──────────┴──────────┴──────────┴──────────────────┘ +``` + +--- + +### Phase 2: Feynman Interrogation (Hunt Pass 1) + +Apply ALL 7 Feynman Question Categories to every function, in priority order from Phase 0. + +**Categories (28+ core questions):** + +``` +Category 1: Purpose — WHY is this line here? What breaks if deleted? +Category 2: Ordering — What if this line moves up/down? State gap window? +Category 3: Consistency — WHY does funcA have this guard but funcB doesn't? +Category 4: Assumptions — What is implicitly trusted about caller/data/state/time? +Category 5: Boundaries — First call, last call, double call, self-reference? +Category 6: Return/Error — Ignored returns, silent failures, fallthrough paths? +Category 7: Call Reorder — Swap external call before/after state update? + + Multi-Tx — Same function, different values, across time? +``` + +For each function: +``` +┌─────────────────────────────────────────────────────────────┐ +│ FUNCTION: [module.functionName] │ +│ Priority: [from Phase 0 hit list] │ +│ │ +│ LINE-BY-LINE INTERROGATION: │ +│ │ +│ L[N]: [code line] │ +│ Q[x.y] → [answer] │ +│ → VERDICT: SOUND | SUSPECT | VULNERABLE │ +│ → If SUSPECT: [specific scenario] │ +│ → STATE FEED: [state variables touched — feed to Phase 3] │ +│ │ +│ FUNCTION VERDICT: SOUND | HAS_CONCERNS | VULNERABLE │ +│ │ +│ SUSPECTS FOR STATE MAPPER (feed to Phase 3): │ +│ - [state var] — [why suspicious from Feynman questioning] │ +│ - [ordering concern] — [which states are in the gap] │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Critical — Category 7 deep checks:** + +For every external call in every function: +1. **Swap test**: Move the external call before/after state updates. Does it revert? If not, the original ordering may be exploitable. +2. **Callee power audit**: At the moment of the external call, what state is committed vs pending? What can the callee observe or manipulate? +3. **Multi-tx state corruption**: Call the function with value X, then again with value Y. Does the second call use stale state from the first? Does accumulated state from many calls create unreachable conditions? + +**Feed forward**: Every SUSPECT verdict and every state variable touched by suspect code is passed to Phase 3 as an additional audit target. + +--- + +### Phase 3: State Cross-Check (Hunt Pass 2) + +The State Mapper now runs its full analysis, ENRICHED by Feynman's Phase 2 output. + +#### 3A: Mutation Matrix + +For EACH state variable (including new ones Feynman flagged): +``` +List every function that modifies it: +- Direct writes, increments, decrements, deletions +- Indirect mutations (internal calls, hooks, callbacks) +- Implicit changes (burns, rebases, external triggers) + +┌──────────────────┬───────────────────┬───────────────────────────┐ +│ State Variable │ Mutating Function │ Updates Coupled State? │ +├──────────────────┼───────────────────┼───────────────────────────┤ +│ [var] │ [function] │ ✓ / ✗ GAP / ??? CHECK │ +└──────────────────┴───────────────────┴───────────────────────────┘ +``` + +#### 3B: Parallel Path Comparison + +``` +Group functions that achieve similar outcomes: +- transfer() vs burn() — both reduce sender balance +- withdraw() vs liquidate() — both reduce position +- partial vs full removal +- direct vs wrapper call +- normal vs emergency/admin path +- single vs batch operation + +For each group: do ALL paths update the SAME coupled state? + +┌─────────────────┬──────────────┬──────────────┬────────────┐ +│ Coupled State │ Path A │ Path B │ Path C │ +├─────────────────┼──────────────┼──────────────┼────────────┤ +│ [state pair] │ ✓/✗ │ ✓/✗ │ ✓/✗ │ +└─────────────────┴──────────────┴──────────────┴────────────┘ +``` + +#### 3C: Operation Ordering Within Functions + +``` +Trace the exact order of state changes in each function: + +step 1: reads A and B → computes result +step 2: modifies B based on result +step 3: modifies A +// B is now stale relative to new A — gap between step 2 and step 3 + +At each step ask: +- Are ALL coupled pairs still consistent RIGHT HERE? +- Does step N use a value that step N-1 already invalidated? +- If an external call happens between steps, can the callee see + inconsistent state? +``` + +#### 3D: Feynman-Enriched Targets + +``` +For each SUSPECT from Phase 2: + → The State Mapper now specifically checks: + 1. Is the suspect state variable part of a coupled pair? + 2. Does the suspect function update all coupled counterparts? + 3. Does the ordering concern from Feynman create a state gap + that the State Mapper can now measure? + +This is where the FEEDBACK LOOP produces findings that NEITHER +auditor would find alone. +``` + +**Feed forward**: Every GAP from Phase 3 is passed to Phase 4 for Feynman re-interrogation. + +--- + +### Phase 4: The Nemesis Loop (FEEDBACK — the core innovation) + +This is what makes Nemesis more than the sum of its parts. The two auditors now interrogate EACH OTHER'S findings. + +``` +LOOP { + ┌─────────────────────────────────────────────────────────┐ + │ STEP A: State Mapper gaps → Feynman re-interrogation │ + │ │ + │ For each GAP found in Phase 3: │ + │ Feynman asks: │ + │ Q: "WHY doesn't [function] update [coupled state B] │ + │ when it modifies [state A]?" │ + │ Q: "What ASSUMPTION is the developer making about │ + │ when [coupled state B] gets updated?" │ + │ Q: "What DOWNSTREAM function reads [state B] and │ + │ would produce a wrong result from the stale value?"│ + │ Q: "Can an attacker CHOOSE a sequence that exploits │ + │ this gap before [state B] gets reconciled?" │ + │ │ + │ → If Feynman finds the gap is real: FINDING │ + │ → If Feynman finds lazy reconciliation: FALSE POSITIVE │ + │ → If Feynman finds a NEW coupled pair: feed back to 3 │ + └─────────────────────────────────────────────────────────┘ + │ + ↓ + ┌─────────────────────────────────────────────────────────┐ + │ STEP B: Feynman findings → State dependency expansion │ + │ │ + │ For each Feynman SUSPECT/VULNERABLE verdict: │ + │ State Mapper asks: │ + │ Q: "Does this suspicious line WRITE to a state that │ + │ is part of a coupled pair I haven't mapped yet?" │ + │ Q: "Does the ordering concern create a WINDOW where │ + │ coupled state is inconsistent?" │ + │ Q: "Does the assumption violation mean a coupled │ + │ state's invariant is based on a false premise?" │ + │ │ + │ → If State Mapper finds new coupling: add to map, │ + │ re-run 3A-3C for the new pair │ + │ → If no new coupling: Feynman finding stands alone │ + └─────────────────────────────────────────────────────────┘ + │ + ↓ + ┌─────────────────────────────────────────────────────────┐ + │ STEP C: Masking code → Joint interrogation │ + │ │ + │ For each defensive/masking pattern found: │ + │ (ternary clamps, min caps, try/catch, early returns) │ + │ │ + │ Feynman asks: "WHY would this ever underflow/overflow? │ + │ What invariant is ACTUALLY broken underneath?" │ + │ │ + │ State Mapper asks: "Which coupled pair's desync is │ + │ this mask hiding? Trace the pair to find the root │ + │ mutation that broke the invariant." │ + │ │ + │ → Combined answer: the mask, the broken invariant, │ + │ the root cause mutation, and the downstream impact │ + └─────────────────────────────────────────────────────────┘ + │ + ↓ + ┌─────────────────────────────────────────────────────────┐ + │ STEP D: Convergence check │ + │ │ + │ Did Steps A-C produce ANY new: │ + │ - Coupled pairs not in the Phase 1 map? │ + │ - Mutation paths not in the Phase 3 matrix? │ + │ - Feynman suspects not in the Phase 2 output? │ + │ - Masking patterns not previously flagged? │ + │ │ + │ IF YES → loop back to STEP A with expanded scope │ + │ IF NO → converged. Proceed to Phase 5. │ + │ │ + │ SAFETY: Maximum 3 loop iterations to prevent runaway. │ + └─────────────────────────────────────────────────────────┘ +} +``` + +--- + +### Phase 5: Multi-Transaction Journey Tracing + +Now that both auditors have converged, trace adversarial sequences that exploit findings from BOTH dimensions. + +``` +For each finding from Phases 2-4, construct a MINIMAL trigger sequence: + +SEQUENCE TEMPLATE: + 1. Initial state (clean) + 2. Operation that modifies State A (coupled to B) + 3. [Optional: time passes / external state evolves] + 4. Operation that SHOULD update B but DOESN'T (the gap) + 5. [Optional: repeat steps 2-4 to compound the error] + 6. Operation that reads BOTH A and B → produces wrong result + +ADVERSARIAL SEQUENCES TO ALWAYS TEST: + - Deposit → partial withdraw → claim rewards + (rewards computed on which balance? old or new?) + + - Stake → unstake half → restake → unstake all + (reward debt accumulated correctly through each step?) + + - Open position → add collateral → partial close → health check + (cached health factor updated at each step?) + + - Provide liquidity → swaps happen → remove liquidity + (fee tracking correct through reserve changes?) + + - Delegate votes → transfer tokens → vote + (voting power reflects current balance?) + + - Borrow → partial repay → borrow again → check debt + (interest accumulator rebased at each step?) + + - Swap with value X → swap with value Y → claim fees + (fee accumulator path-dependent? See worked example below) + +MULTI-TX STATE CORRUPTION — WORKED EXAMPLE: + ───────────────────────────────────────── + AMM pool with swap() that: + 1. Calculates amountOut based on reserves + 2. Updates accumulatedFees (for LP fee distribution) + 3. Updates reserves + + TX1: Alice swaps 1000 tokenA → tokenB (0.3% fee) + - fee = 3 tokenA added to accFees BEFORE reserves update + - reserves shift: reserveA=11000, reserveB≈9091 + + TX2: Bob swaps 500 tokenA → tokenB + - fee = 1.5 tokenA added to accFees + - feePerLP calculated using STALE reserve ratio from pre-TX1 + - 1 tokenA is now worth LESS in the pool, but fee accounting + doesn't know that + + TX3: Charlie claims LP fees + - Gets paid based on accFees=4.5 at OLD token valuation + - Pool composition has shifted — fees are denominated in a + token whose relative value changed + - Result: early LPs overpaid, late LPs underpaid + + Root cause: accFees accumulator doesn't rebase against current + reserve ratio. Each swap changes what "1 unit of fee" means, + but the accumulator treats all units as equal. + + GENERALIZE: Any global accumulator (fees, rewards, interest) + updated per-tx where the VALUE of what's accumulated changes + between txs, and the accumulator doesn't normalize. + + CHECK: After N operations with varying sizes, does + SUM(individual fees) == fee on AGGREGATE operation? + If not → path-dependent accumulator → exploitable. +``` + +--- + +### Phase 6: Verification Gate (MANDATORY) + +**Every CRITICAL, HIGH, and MEDIUM finding MUST be verified.** + +#### Methods: + +**Method A: Deep Code Trace** +1. Read exact lines cited +2. Trace complete call chain (caller → callee → downstream) +3. Check for mitigating code elsewhere (guards, hooks, lazy reconciliation) +4. Confirm scenario is reachable end-to-end +5. Verdict: TRUE POSITIVE / FALSE POSITIVE / DOWNGRADE + +**Method B: PoC Test** +1. Write test in project's native framework +2. Execute the exact trigger sequence from the finding +3. Assert state inconsistency after the breaking operation +4. Assert incorrect result in the downstream operation +5. Verdict: TRUE POSITIVE / FALSE POSITIVE + +**Method C: Hybrid** (trace + PoC) for complex multi-module findings. + +#### Common False Positive Patterns (from BOTH auditors): + +``` +1. HIDDEN RECONCILIATION: Coupled state IS updated, but through an + internal call chain you missed (_beforeTokenTransfer hook, modifier + that runs _updateReward before every function). + +2. LAZY EVALUATION: Coupled state is intentionally stale and reconciled + on next READ, not on every WRITE. The desync is by design. + +3. IMMUTABLE AFTER INIT: The coupled state is set once and never needs + updating because both sides are frozen after initialization. + +4. DESIGNED ASYMMETRY: The states are intentionally NOT coupled the way + you assumed. Read docs/comments before reporting. + +5. LANGUAGE SAFETY: Finding claims overflow but the language aborts on + overflow by default (Solidity >=0.8, Move, Rust debug). + +6. SEVERITY INFLATION: Finding claims "value loss" but actual impact is + "confusing error message" because a downstream check catches it. + +7. ECONOMIC INFEASIBILITY: The attack costs more than it gains. + Flash loans don't make everything free — compute the actual profit. +``` + +#### Verification Output per Finding: + +``` +Finding NM-XXX: [Title] +├─ Verification method: [A / B / C] +├─ Code trace: [paths traced, mitigations checked] +├─ PoC result: [test name, pass/fail, key output] +├─ Mitigating factors found: [none / list] +└─ VERDICT: TRUE POSITIVE [severity] / FALSE POSITIVE [reason] / DOWNGRADE [from→to] +``` + +--- + +### Phase 7: Final Report + +Save to: `.audit/findings/nemesis-verified.md` + +```markdown +# N E M E S I S — Verified Findings + +## Scope +- Language: [detected] +- Modules analyzed: [list] +- Functions analyzed: [count] +- Coupled state pairs mapped: [count] +- Mutation paths traced: [count] +- Nemesis loop iterations: [count] + +## Nemesis Map (Phase 1 Cross-Reference) +[Unified map: functions × state × couplings × gaps] + +## Verification Summary +| ID | Source | Coupled Pair | Breaking Op | Severity | Verdict | +|----|--------|-------------|-------------|----------|---------| +| NM-001 | Feynman→State | A↔B | func() | HIGH | TRUE POS | +| NM-002 | State→Feynman | C↔D | func2() | MEDIUM | TRUE POS | +| NM-003 | Loop Step C | E↔F | func3() | HIGH | DOWNGRADE→MED | +| NM-004 | Feynman only | — | func4() | MEDIUM | FALSE POS | + +## Verified Findings (TRUE POSITIVES only) + +### Finding NM-001: [Title] +**Severity:** CRITICAL | HIGH | MEDIUM | LOW +**Source:** [Which auditor found it, or "Feedback Loop Step X"] +**Verification:** [Code trace / PoC / Hybrid] + +**Coupled Pair:** State A ↔ State B +**Invariant:** [What relationship must hold] + +**Feynman Question that exposed it:** +> [The exact question] + +**State Mapper gap that confirmed it:** +> [The mutation matrix entry showing the missing update] + +**Breaking Operation:** `functionName()` at `File.sol:L123` +- Modifies State A: [how] +- Does NOT update State B: [what's missing] + +**Trigger Sequence:** +1. [Step-by-step] +2. [Minimal adversarial sequence] + +**Consequence:** +- [What goes wrong downstream] +- [Concrete impact with numbers] + +**Masking Code** (if present): +```[language] +// This defensive code hides the broken invariant: +[code] +``` + +**Verification Evidence:** +[Code trace paths / PoC test output / concrete numbers] + +**Fix:** +```[language] +// Add the missing state synchronization: +[minimal fix] +``` + +--- + +## Feedback Loop Discoveries +[Findings that ONLY emerged from the cross-feed between auditors — +bugs that neither Feynman alone nor State Mapper alone would have found] + +## False Positives Eliminated +[Findings that failed verification, with explanation] + +## Downgraded Findings +[Findings where severity was reduced, with justification] + +## Summary +- Total functions analyzed: [N] +- Coupled state pairs mapped: [N] +- Nemesis loop iterations: [N] +- Raw findings (pre-verification): [N] C | [N] H | [N] M | [N] L +- Feedback loop discoveries: [N] (found ONLY via cross-feed) +- After verification: [N] TRUE POSITIVE | [N] FALSE POSITIVE | [N] DOWNGRADED +- Final: [N] CRITICAL | [N] HIGH | [N] MEDIUM | [N] LOW +``` + +Also save intermediate work to: `.audit/findings/nemesis-raw.md` + +--- + +## Red Flags Checklist (Combined) + +``` +FROM FEYNMAN: +- [ ] A line of code whose PURPOSE you cannot explain +- [ ] An ordering choice with no clear justification +- [ ] A guard on funcA that's missing from funcB (same state) +- [ ] An implicit trust assumption about caller/data/state/time +- [ ] External call with state updates AFTER it (stale state window) +- [ ] Function behaves differently on 2nd call due to 1st call's state change + +FROM STATE MAPPER: +- [ ] Function modifies State A but has no writes to coupled State B +- [ ] Two similar operations handle coupled state differently +- [ ] Claim/collect runs before reduce/remove with no reconciliation +- [ ] Partial operation exists but only full operation resets coupled state +- [ ] Defensive ternary/min() between two coupled values (WHY underflow?) +- [ ] delete/reset of one mapping but not its paired mapping +- [ ] Loop accumulates into shared state without per-iteration adjustment +- [ ] Emergency/admin function bypasses normal state update path + +FROM THE FEEDBACK LOOP: +- [ ] Feynman found an ordering concern + State Mapper found a gap in the + SAME function → compound finding +- [ ] State Mapper found masking code + Feynman explained WHY the invariant + is broken underneath → root cause finding +- [ ] Feynman found an assumption about state freshness + State Mapper + confirmed the state IS stale after a specific mutation path +- [ ] Both auditors flagged the SAME function from different angles + → highest confidence finding +``` + +--- + +## Severity Classification + +| Severity | Criteria | +|----------|----------| +| **CRITICAL** | Direct value loss, permanent DoS, or system insolvency. Exploitable now. | +| **HIGH** | Conditional value loss, privilege escalation, or broken core invariant | +| **MEDIUM** | Value leakage, griefing with cost, incorrect accounting, degraded functionality | +| **LOW** | Informational, cosmetic inconsistency, edge-case-only with no material impact | + +--- + +## Post-Audit Actions + +| Scenario | Action | +|----------|--------| +| Need deeper protocol context | Re-read the relevant contracts and documentation | +| Finding needs formal report | Write up with severity, trigger sequence, PoC, and fix | +| Need exploit validation | Write a Foundry/Hardhat PoC test to confirm | +| Uncertain about design intent | Check NatSpec, comments, and project documentation | + +--- + +## Anti-Hallucination Protocol + +``` +NEVER: +- Invent code that doesn't exist in the codebase +- Assume a coupled pair without finding code that reads BOTH values together +- Claim a function is missing an update without tracing its full call chain +- Report a finding without the exact code, trigger sequence, AND consequence +- Force Solidity terminology onto non-Solidity code +- Skip the feedback loop (Phase 4) — it's where the highest-value bugs emerge +- Present raw findings as verified results + +ALWAYS: +- Read actual code before questioning it +- Verify coupled pairs by finding code that reads BOTH values +- Trace internal calls for hidden updates (hooks, modifiers, base classes) +- Check for lazy reconciliation patterns before reporting stale state +- Show exact file paths and line numbers +- Run the feedback loop until convergence +- Present ONLY verified findings in the final report +``` + +--- + +## Quick-Start Checklist + +``` +- [ ] Phase 0: Attacker recon (goals, novel code, value stores, coupling hypothesis) +- [ ] Phase 1A: Build Function-State Matrix +- [ ] Phase 1B: Build Coupled State Dependency Map +- [ ] Phase 1C: Cross-reference → Unified Nemesis Map +- [ ] Phase 2: Feynman interrogation (all 7 categories, priority order) +- [ ] Phase 2: Feed all SUSPECT verdicts to Phase 3 +- [ ] Phase 3A: Build Mutation Matrix (enriched by Feynman suspects) +- [ ] Phase 3B: Parallel Path Comparison +- [ ] Phase 3C: Operation Ordering check +- [ ] Phase 3D: Feynman-Enriched Target analysis +- [ ] Phase 4: THE NEMESIS LOOP +- [ ] Step A: State gaps → Feynman re-interrogation +- [ ] Step B: Feynman findings → State dependency expansion +- [ ] Step C: Masking code → Joint interrogation +- [ ] Step D: Convergence check (loop if new findings, max 3 iterations) +- [ ] Phase 5: Multi-transaction journey tracing (adversarial sequences) +- [ ] Phase 6: Verify ALL C/H/M findings (code trace + PoC) +- [ ] Phase 6: Eliminate false positives +- [ ] Phase 7: Save to .audit/findings/nemesis-verified.md +- [ ] Phase 7: Present ONLY verified findings +``` diff --git a/.claude/skills/state-inconsistency-auditor/SKILL.md b/.claude/skills/state-inconsistency-auditor/SKILL.md new file mode 100644 index 00000000..39c3c94e --- /dev/null +++ b/.claude/skills/state-inconsistency-auditor/SKILL.md @@ -0,0 +1,517 @@ +--- +name: state-inconsistency-auditor +description: Finds state inconsistency bugs where an operation mutates one piece of coupled state without updating its dependent counterpart, causing silent data corruption or reverts in subsequent operations. Triggers on /state-audit, state inconsistency audit, or coupled state audit. +--- + +# State Inconsistency Auditor + +Finds bugs where an operation mutates one piece of coupled state without updating its dependent counterpart, causing silent data corruption or reverts in subsequent operations. + +**Language-agnostic by design.** Coupled state bugs exist in any system that maintains related storage values — Solidity, Move, Rust, Go, C++, or anything else. + +This agent performs **structural invariant analysis** — systematically mapping every coupled state pair, every mutation path, and every gap where one side updates without the other. It complements first-principles reasoning (Feynman) and pattern-matching tools by finding structural state desync bugs that other methodologies miss. + +## When to Activate + +- User says "/state-audit" or "state inconsistency audit" or "coupled state audit" +- User wants to find stale state, broken invariants, or desynchronized storage +- After any other audit methodology to catch what it missed + +## When NOT to Use + +- Quick pattern-matching scans where you only need known vulnerability patterns +- First-principles logic bugs only (use `/feynman` instead) +- Simple spec compliance checks +- Report generation from existing findings + +--- + +## The Abstract Pattern + +Every system has **COUPLED STATE PAIRS** — two or more storage values that must maintain a relationship (an invariant) with each other. When any operation changes one side of the pair without adjusting the other, the invariant breaks. Future operations that read both values produce incorrect results. + +Examples of coupled state: +- balance & checkpoint +- position size & accumulated tracker +- collateral & obligation +- voting power & snapshot +- shares & any per-share derived value +- principal & any cumulative index +- totalSupply & sum of all individual balances +- debt & interest accumulator +- liquidity & fee growth trackers +- stake amount & reward debt +- token balance & voting delegation +- position collateral & position health factor cache + +**The bug class:** Operation X correctly updates State A, but fails to proportionally adjust the coupled State B. State B is now stale relative to State A. + +--- + +## Language Adaptation + +When you start, **detect the language** and adapt terminology: + +| Concept | Solidity | Move | Rust | Go | C++ | +|---------|----------|------|------|----|-----| +| Storage | state variables | global storage / resources | struct fields / state | struct fields / DB | member variables | +| Mapping | mapping(k => v) | Table\ / SmartTable | HashMap / BTreeMap | map[K]V | std::map / unordered_map | +| Delete | delete mapping[key] | table::remove | map.remove(&key) | delete(map, key) | map.erase(key) | +| Event | emit Event() | event::emit() | emit! / log | EventEmit() | signal / callback | +| Internal call | internal function | friend function | pub(crate) fn | unexported func | private method | + +--- + +## Core Rules + +``` +RULE 0: MAP BEFORE YOU HUNT +Never start checking functions until you have the complete coupled state +dependency map. You cannot find a missing update if you don't know what +updates are required. + +RULE 1: EVERY MUTATION PATH MATTERS +A state variable might be modified by 5 different functions. ALL 5 must +update the coupled state. If 4 do and 1 doesn't — that's the bug. + +RULE 2: PARTIAL OPERATIONS ARE THE #1 SOURCE +Full removals (delete everything) usually reset all state correctly. +Partial operations (reduce by X) frequently forget to proportionally +reduce the coupled state. + +RULE 3: COMPARE PARALLEL PATHS +If transfer() and burn() both reduce a balance, they MUST both update +the same set of coupled state. If one does and the other doesn't — finding. + +RULE 4: DEFENSIVE CODE MASKS BUGS +Code like `x > y ? x - y : 0` or `min(computed, available)` silently +hides broken invariants. These are red flags, not safety nets. + +RULE 5: EVIDENCE-BASED FINDINGS ONLY +Every finding must include: the coupled pair, the breaking operation, +a concrete trigger sequence, and the downstream consequence. +``` + +--- + +## Audit Process + +### Phase 1: Map All Coupled State Pairs + +For every storage variable, ask: **"What other storage values must change when this one changes?"** + +Build a dependency map: + +``` +State A changes → State B MUST also change (and vice versa) +State C changes → State D and State E MUST also change +``` + +Look for: +- Any per-user value paired with any per-user accumulator/tracker +- Any balance paired with any historical snapshot or checkpoint +- Any numerator paired with its denominator +- Any position-describing value paired with any position-derived value +- Anything stored at time T that is later used with a value from time T+1 +- Any total/aggregate paired with individual components that sum to it +- Any cached computation paired with the inputs it was derived from +- Any index/accumulator paired with the last-known snapshot of that index + +**Output of Phase 1:** A Coupled State Dependency Map. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ COUPLED STATE DEPENDENCY MAP │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ PAIR 1: userBalance[user] ↔ checkpoint[user] │ +│ Invariant: checkpoint must reflect balance at last update │ +│ Mutation points: deposit(), withdraw(), transfer(), burn() │ +│ │ +│ PAIR 2: totalStaked ↔ rewardPerTokenStored │ +│ Invariant: rewardPerToken must be updated before │ +│ totalStaked changes │ +│ Mutation points: stake(), unstake(), emergencyWithdraw() │ +│ │ +│ PAIR 3: position.collateral ↔ position.debtShares │ +│ Invariant: health factor derived from both must stay valid │ +│ Mutation points: addCollateral(), borrow(), repay(), │ +│ liquidate(), withdrawCollateral() │ +│ │ +│ ... │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +### Phase 2: Find Every Operation That Mutates Each State + +For EACH state variable identified in Phase 1, list **every** function and code path that modifies it. Include: + +- **Direct writes**: `state = newValue` +- **Increments/decrements**: `state += delta`, `state -= delta` +- **Deletions**: `delete state`, `state = 0`, `state = default` +- **Indirect mutations**: calling a function that internally modifies it (e.g., `_mint()`, `_burn()`, `_transfer()`) +- **Implicit changes**: burning reduces balance via internal `_burn`, rebasing changes effective balance without explicit write +- **Batch operations**: loops or multicalls that modify state multiple times +- **External triggers**: callbacks, hooks, or oracle updates that modify state as a side effect + +**Output of Phase 2:** A Mutation Matrix. + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ MUTATION MATRIX │ +├──────────────────┬───────────────────┬───────────────────────────┤ +│ State Variable │ Mutating Function │ Type of Mutation │ +├──────────────────┼───────────────────┼───────────────────────────┤ +│ userBalance[u] │ deposit() │ increment (+= amount) │ +│ userBalance[u] │ withdraw() │ decrement (-= amount) │ +│ userBalance[u] │ transfer() │ decrement sender, inc recv │ +│ userBalance[u] │ _burn() │ decrement (-= amount) │ +│ userBalance[u] │ liquidate() │ decrement (-= seized) │ +│ checkpoint[u] │ deposit() │ full reset │ +│ checkpoint[u] │ withdraw() │ full reset │ +│ checkpoint[u] │ transfer() │ ??? — CHECK THIS │ +│ checkpoint[u] │ _burn() │ ??? — CHECK THIS │ +│ checkpoint[u] │ liquidate() │ ??? — CHECK THIS │ +└──────────────────┴───────────────────┴───────────────────────────┘ +``` + +The `???` entries are your **primary audit targets** — mutations of State A where you haven't confirmed State B is also updated. + +--- + +### Phase 3: Cross-Check — The Core Audit + +For EVERY (operation, state variable) pair from Phase 2: + +> "This operation modifies State A. +> Does it ALSO update every coupled state that depends on A?" + +Check specifically: + +``` +□ Full removal (A → 0): Is every coupled state reset/cleared? +□ Partial removal (A decreases): Is every coupled state proportionally reduced? +□ Increase (A grows): Is every coupled state proportionally increased? +□ Transfer (A moves between entities): Is coupled state moved too? +□ Deletion (mapping entry removed): Is the paired mapping entry also removed? +□ Batch modification: Is coupled state updated per-iteration or only once? +``` + +**If ANY path updates A without updating its coupled state → FINDING.** + +For each potential finding, trace the FULL code path: +1. Read the function that modifies State A +2. Search for any write to State B within the same function +3. Search for any internal call that writes to State B +4. Search for any modifier/hook that writes to State B +5. If none found → confirmed finding + +--- + +### Phase 4: Check Operation Ordering Within Functions + +Many functions perform multiple state changes sequentially. Trace the exact order: + +``` +function doSomething() { + step1: reads State A and State B → computes result + step2: modifies State B based on result + step3: modifies State A + // State B is now stale relative to new State A +} +``` + +Ask at each step: +- "After this step, are ALL coupled pairs still consistent?" +- "Does step N use a value that step N-1 already invalidated?" +- "If I read the coupled pair RIGHT HERE, would the invariant hold?" +- "If an external call happens between step N and step N+1, can the callee observe inconsistent state?" + +**Common ordering bugs:** +- Claim rewards BEFORE reducing stake → rewards computed on old (higher) stake +- Update index AFTER modifying supply → index uses stale supply +- Read cached price AFTER changing position → health check uses wrong price +- Emit event with old values AFTER state change → off-chain systems desync + +--- + +### Phase 5: Compare Parallel Code Paths + +Find operations that achieve similar outcomes through different paths: + +- `transfer()` vs `burn()` — both reduce sender balance +- `withdraw()` vs `liquidate()` — both reduce position +- `partial` vs `full` removal — both decrease, different amounts +- Direct call vs routed-through-wrapper call +- Normal path vs emergency/admin path +- Single operation vs batch operation +- User-initiated vs keeper/bot-initiated + +For each group, compare: **do ALL paths update the same coupled state?** + +``` +┌────────────────────────────────────────────────────────────┐ +│ PARALLEL PATH COMPARISON │ +├─────────────────┬──────────────┬──────────────┬────────────┤ +│ Coupled State │ withdraw() │ liquidate() │ emergencyW │ +├─────────────────┼──────────────┼──────────────┼────────────┤ +│ balance │ ✓ updated │ ✓ updated │ ✓ updated │ +│ checkpoint │ ✓ updated │ ✗ MISSING │ ✗ MISSING │ +│ totalSupply │ ✓ updated │ ✓ updated │ ✗ MISSING │ +│ rewardDebt │ ✓ updated │ ✗ MISSING │ ✗ MISSING │ +└─────────────────┴──────────────┴──────────────┴────────────┘ + +FINDINGS: liquidate() and emergencyWithdraw() don't update checkpoint + or rewardDebt when reducing balance. +``` + +If Path A adjusts the coupled state but Path B doesn't → **FINDING**. + +--- + +### Phase 6: Trace Multi-Step User Journeys + +Simulate sequences where a user interacts multiple times: + +``` +1. User enters a position (state initialized) +2. Time passes / external state evolves (index grows, prices change) +3. User does PARTIAL modification (coupled state may break here) +4. More time passes / external state evolves +5. User does another operation reading the coupled state +``` + +At step 5, ask: +- "Is the coupled state still valid given the partial change at step 3?" +- "Does the computation use stale State B with current State A?" +- "If State B wasn't updated at step 3, how much error has accumulated by step 5?" + +**Key sequences to test:** +- Deposit → partial withdraw → claim rewards (rewards on correct amount?) +- Stake → unstake half → restake → unstake all (reward debt correct?) +- Open position → add collateral → partial close → check health (cached values fresh?) +- Delegate votes → transfer tokens → vote (voting power reflects current balance?) +- Provide liquidity → swap happens → remove liquidity (fee tracking correct?) + +--- + +### Phase 7: Check What Masks the Bug + +Look for defensive code that HIDES broken invariants: + +``` +MASKING PATTERN 1: Ternary clamp + x > y ? x - y : 0 + → WHY would x ever be less than y? If the invariant held, it wouldn't. + This silently returns 0 instead of reverting on the broken state. + +MASKING PATTERN 2: Try/catch swallowing + try target.call() {} catch {} + → The revert from broken state is caught and ignored. + +MASKING PATTERN 3: Early exit on zero + if (value == 0) return; + → Skips the computation entirely when the broken state produces zero. + +MASKING PATTERN 4: Min cap + min(computed, available) + → Caps the result when broken state over-counts. The over-counting + is the bug; the min() just prevents the revert. + +MASKING PATTERN 5: SafeMath without root cause fix + → Prevents underflow revert but doesn't fix WHY the subtraction + would underflow. The state is still inconsistent. + +MASKING PATTERN 6: Fallback to default + value = mapping[key] // returns 0 for non-existent key + → If the key SHOULD exist but was deleted without cleaning its + coupled entry, the zero default masks the missing data. +``` + +**These patterns convert what SHOULD be a loud failure into a silent one.** The invariant is still broken — the symptom is just suppressed. Flag every instance and trace whether the defensive code is hiding a real state inconsistency. + +--- + +## Red Flags Checklist + +``` +- [ ] A function modifies a base value but has no writes to its coupled state +- [ ] Two similar operations (e.g., transfer vs burn) handle coupled state differently +- [ ] A "claim/collect" step runs before a "reduce/remove" step, and + nothing reconciles afterward +- [ ] Partial operations exist alongside full operations, but only the full + operation resets/clears the coupled state +- [ ] A defensive ternary or min() exists in a computation involving two + coupled values (asks: WHY would this ever underflow?) +- [ ] delete or reset of one mapping but not its paired mapping +- [ ] A loop processes multiple sub-positions but accumulates into a + shared coupled value without per-iteration adjustment +- [ ] An emergency/admin function bypasses the normal state update path +- [ ] A migration or upgrade function copies State A but not State B +- [ ] A callback or hook modifies State A but the caller doesn't know + to update State B afterward +``` + +--- + +## Phase 8: Verification Gate (MANDATORY) + +**Every CRITICAL, HIGH, and MEDIUM finding MUST be verified before final report.** + +### Verification Methods: + +**Method A: Code Trace Verification** +1. Read the exact function that breaks the invariant +2. Trace every internal call — confirm no hidden update to the coupled state +3. Check modifiers, hooks, and base class overrides +4. Confirm no event-driven or callback-based reconciliation exists +5. Verdict: TRUE POSITIVE / FALSE POSITIVE + +**Method B: PoC Test Verification** +1. Write a test using the project's native framework +2. Execute the trigger sequence from the finding +3. Assert that the coupled state is inconsistent after the operation +4. Assert that a subsequent operation produces incorrect results +5. If test passes: TRUE POSITIVE. If test fails: FALSE POSITIVE + +**Method C: Hybrid (trace + PoC)** +For complex multi-contract findings spanning multiple modules. + +### Common False Positive Patterns: + +1. **Hidden reconciliation**: The coupled state IS updated, but through an internal call chain you missed (e.g., `_beforeTokenTransfer` hook). +2. **Lazy evaluation**: The coupled state is intentionally stale and reconciled on next read (e.g., `_updateReward()` modifier runs before every function). +3. **Immutable after init**: The coupled state is set once and never needs updating because State A also never changes after init. +4. **Designed asymmetry**: The two states are intentionally NOT coupled in the way you assumed (read the docs/comments). + +--- + +## Severity Classification + +| Severity | Criteria | +|----------|----------| +| **CRITICAL** | Coupled state desync causes direct value loss (wrong payouts, stolen funds, permanent lock) | +| **HIGH** | Coupled state desync causes conditional value loss or broken core functionality | +| **MEDIUM** | Coupled state desync causes incorrect accounting, griefing, or degraded functionality | +| **LOW** | Coupled state desync causes cosmetic issues, event inaccuracy, or edge-case-only errors | + +--- + +## Output Format + +Save raw findings to: `.audit/findings/state-inconsistency-raw.md` +Save verified findings to: `.audit/findings/state-inconsistency-verified.md` + +```markdown +# State Inconsistency Audit — Verified Findings + +## Coupled State Dependency Map +[The map from Phase 1] + +## Mutation Matrix +[The matrix from Phase 2] + +## Parallel Path Comparison +[The comparison table from Phase 5] + +## Verification Summary +| ID | Coupled Pair | Breaking Op | Original Severity | Verdict | Final Severity | +|----|-------------|-------------|-------------------|---------|----------------| + +## Verified Findings + +### Finding SI-001: [Title] +**Severity:** CRITICAL | HIGH | MEDIUM | LOW +**Verification:** [Code trace / PoC / Hybrid] + +**Coupled Pair:** State A ↔ State B +**Invariant:** [What relationship must hold between them] + +**Breaking Operation:** `functionName()` in `Contract.sol:L123` +- Modifies State A: [how] +- Does NOT update State B: [what's missing] + +**Trigger Sequence:** +1. [Step-by-step minimal sequence to break the invariant] + +**Consequence:** +- [What goes wrong when a later operation reads both A and B] +- [Concrete impact: wrong payout amount, locked funds, etc.] + +**Masking Code** (if present): +```[language] +// This defensive code hides the broken invariant: +[the masking pattern] +``` + +**Fix:** +```[language] +// Add the missing state synchronization: +[minimal fix] +``` + +--- + +## False Positives Eliminated +[Findings that failed verification, with explanation] + +## Summary +- Coupled state pairs mapped: [N] +- Mutation paths analyzed: [N] +- Raw findings (pre-verification): [N] +- After verification: [N] TRUE POSITIVE | [N] FALSE POSITIVE +- Final: [N] CRITICAL | [N] HIGH | [N] MEDIUM | [N] LOW +``` + +--- + +## Post-Audit Actions + +| Scenario | Action | +|----------|--------| +| Need deeper context on a function | Re-read the function and its callers line-by-line | +| Finding confirmed as true positive | Write up with severity, trigger sequence, PoC, and fix | +| Need first-principles reasoning on a pair | Run `/feynman` on the specific functions involved | +| Need exploit validation | Write a Foundry/Hardhat PoC test to confirm | +| Uncertain about design intent | Check NatSpec, comments, and project documentation | + +--- + +## Anti-Hallucination Protocol + +``` +NEVER: +- Assume two states are coupled without verifying they are read together +- Claim a function is missing an update without reading its full call chain +- Report a finding without showing the exact code that breaks the invariant +- Ignore lazy-evaluation patterns (modifiers that reconcile on entry) +- Assume a mapping deletion is a bug without checking if the paired mapping + is also deleted or intentionally kept + +ALWAYS: +- Read the actual storage declarations to understand types and relationships +- Trace internal calls to check for hidden updates +- Check _before/_after hooks and modifiers for reconciliation logic +- Verify the coupled relationship by finding code that reads BOTH values together +- Show exact file paths and line numbers for all references +``` + +--- + +## Quick-Start Checklist + +- [ ] **Phase 1:** Map all storage variables and their coupled dependencies +- [ ] **Phase 1:** Build the Coupled State Dependency Map +- [ ] **Phase 2:** For each state variable, list every mutating function +- [ ] **Phase 2:** Build the Mutation Matrix (mark `???` for unconfirmed updates) +- [ ] **Phase 3:** Cross-check every mutation — does it update all coupled state? +- [ ] **Phase 4:** Check operation ordering within each function +- [ ] **Phase 5:** Compare parallel code paths (transfer/burn, withdraw/liquidate, etc.) +- [ ] **Phase 6:** Trace multi-step user journeys for stale state accumulation +- [ ] **Phase 7:** Flag all defensive/masking code and trace whether it hides broken invariants +- [ ] **Phase 8:** Verify ALL C/H/M findings (code trace + PoC) +- [ ] **Phase 8:** Eliminate false positives (hidden reconciliation, lazy eval, designed asymmetry) +- [ ] **Phase 8:** Save verified findings to `.audit/findings/state-inconsistency-verified.md` +- [ ] **Phase 8:** Present ONLY verified report to the user diff --git a/.openzeppelin/mainnet.json b/.openzeppelin/mainnet.json index ba56e812..46c7f24b 100644 --- a/.openzeppelin/mainnet.json +++ b/.openzeppelin/mainnet.json @@ -1712,8 +1712,8 @@ } }, "8dcb1ee047f8720601ec56f8a96045de3d207b3cea6564105d3ee438a30d5802": { - "address": "0xefED40D1eb1577d1073e9C4F277463486D39b084", - "txHash": "0x4e820b8e15298d5b1fbe30e151e0a0b16c0c23948e28dc84ae9a52280104b5be", + "address": "0xD4998Cc1ba435298C521f250b81856B1F25C8455", + "txHash": "0xf51d10838d35cb2d97fd55f1c32c3399384133dd95ca2043125666c9245bb5de", "layout": { "solcVersion": "0.8.9", "storage": [ @@ -1823,20 +1823,36 @@ "src": "contracts/access/WithMidasAccessControl.sol:24" }, { - "label": "metadata", + "label": "__gap", "offset": 0, "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:16" + }, + { + "label": "metadata", + "offset": 0, + "slot": "302", "type": "t_mapping(t_bytes32,t_bytes_storage)", "contract": "mTBILL", - "src": "contracts/mTBILL.sol:29" + "src": "contracts/mTBILL/mTBILL.sol:18" }, { "label": "__gap", "offset": 0, - "slot": "203", + "slot": "303", "type": "t_array(t_uint256)50_storage", "contract": "mTBILL", - "src": "contracts/mTBILL.sol:34" + "src": "contracts/mTBILL/mTBILL.sol:23" } ], "types": { diff --git a/.openzeppelin/sepolia.json b/.openzeppelin/sepolia.json index 90d50f25..fe692d48 100644 --- a/.openzeppelin/sepolia.json +++ b/.openzeppelin/sepolia.json @@ -784,6 +784,46 @@ "address": "0x75726da161ef6aE712e941E00705879715b260f6", "txHash": "0xcb8a84e91a264b44fc00c807ea3736e68d2fa84562889cbd9062d8747963febb", "kind": "transparent" + }, + { + "address": "0xbf21e448410BAA6039e73033F530027143c0c280", + "txHash": "0xe7d50e32586a9b999bd075b487036652d3bbfb3395c6c565c89335a5a828e162", + "kind": "transparent" + }, + { + "address": "0xc6Ad27a2446Aa6223512D9FF8A6f3440a20ccd12", + "txHash": "0x174016bd9ade21fb7b15606e276313cacf21ebd836231b8371e320c386152573", + "kind": "transparent" + }, + { + "address": "0x982550a433239C23BFe6C57005A7396D2Ed706d2", + "txHash": "0x630698c2dd09bfe7b4c2570ecc17cd547d4045909f351407d7348d55bb8b8b02", + "kind": "transparent" + }, + { + "address": "0x9393811adDd7F7Ff60D1be11DbD29025A15bf630", + "txHash": "0x0b6bf43b19dc9ca9fda56bc73d1bd8d8913eec82f01a7f5b021ff9eb3e02d7fb", + "kind": "transparent" + }, + { + "address": "0x2Abf7B5766Fc75bdb99e7aD76d6B539D08F3f8E1", + "txHash": "0x4964eb928bffa347c2db9a3d25da2ba1084bde4b7d5c6c7d5c85d0877f3b33ae", + "kind": "transparent" + }, + { + "address": "0x95059CaD850a0531dd9b086F019C7f1ACB15955c", + "txHash": "0x6bc9c4a7a2e50a05fc70dd3b41dd21e15b6ffbcc249541b79abf729debefb2f4", + "kind": "transparent" + }, + { + "address": "0x95AACf1bAD48336B0a62E90C2799fA8623CD9128", + "txHash": "0x2e0e2e3d93a9194c18d04c1898b65651964f14613a2303c0d02c3d358cda1265", + "kind": "transparent" + }, + { + "address": "0x4e830D858c253C81ACF225E7101DF820D0e98415", + "txHash": "0x30daf803fd617c6734d733c5d48dc5bda3ca82784b153062caa086e8d7a50219", + "kind": "transparent" } ], "impls": { @@ -3237,20 +3277,36 @@ "src": "contracts/access/WithMidasAccessControl.sol:24" }, { - "label": "metadata", + "label": "__gap", "offset": 0, "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:16" + }, + { + "label": "metadata", + "offset": 0, + "slot": "302", "type": "t_mapping(t_bytes32,t_bytes_storage)", "contract": "mTBILL", - "src": "contracts/mTBILL.sol:29" + "src": "contracts/mTBILL/mTBILL.sol:18" }, { "label": "__gap", "offset": 0, - "slot": "203", + "slot": "303", "type": "t_array(t_uint256)50_storage", "contract": "mTBILL", - "src": "contracts/mTBILL.sol:34" + "src": "contracts/mTBILL/mTBILL.sol:23" } ], "types": { @@ -46498,6 +46554,5727 @@ } } }, + "3ef1a8831ff39394090b1cd20d6b0082bde8f25035625f08aece01b4a32fb6ee": { + "address": "0x3256a123Ea7FAfc783b15c158D60db5eFB9895Ed", + "txHash": "0x1dc32471e107d70d4f672d132513d0cfc653a8f134913bd5f5899f82a22c5e8f", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_roles", + "offset": 0, + "slot": "101", + "type": "t_mapping(t_bytes32,t_struct(RoleData)9957_storage)", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:62" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:260" + }, + { + "label": "isUserFacingRole", + "offset": 0, + "slot": "151", + "type": "t_mapping(t_bytes32,t_bool)", + "contract": "MidasAccessControl", + "src": "contracts/access/MidasAccessControl.sol:29" + }, + { + "label": "_grantOperatorRoles", + "offset": 0, + "slot": "152", + "type": "t_mapping(t_bytes32,t_mapping(t_address,t_bool))", + "contract": "MidasAccessControl", + "src": "contracts/access/MidasAccessControl.sol:34" + }, + { + "label": "_permissionRoles", + "offset": 0, + "slot": "153", + "type": "t_mapping(t_bytes32,t_mapping(t_address,t_bool))", + "contract": "MidasAccessControl", + "src": "contracts/access/MidasAccessControl.sol:39" + }, + { + "label": "_roleTimelockDelays", + "offset": 0, + "slot": "154", + "type": "t_mapping(t_bytes32,t_uint32)", + "contract": "MidasAccessControl", + "src": "contracts/access/MidasAccessControl.sol:44" + }, + { + "label": "timelockManager", + "offset": 0, + "slot": "155", + "type": "t_address", + "contract": "MidasAccessControl", + "src": "contracts/access/MidasAccessControl.sol:49" + }, + { + "label": "pauseManager", + "offset": 0, + "slot": "156", + "type": "t_address", + "contract": "MidasAccessControl", + "src": "contracts/access/MidasAccessControl.sol:54" + }, + { + "label": "defaultDelay", + "offset": 20, + "slot": "156", + "type": "t_uint32", + "contract": "MidasAccessControl", + "src": "contracts/access/MidasAccessControl.sol:59" + }, + { + "label": "__gap", + "offset": 0, + "slot": "157", + "type": "t_array(t_uint256)50_storage", + "contract": "MidasAccessControl", + "src": "contracts/access/MidasAccessControl.sol:64" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bool)": { + "label": "mapping(bytes32 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_mapping(t_address,t_bool))": { + "label": "mapping(bytes32 => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)9957_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint32)": { + "label": "mapping(bytes32 => uint32)", + "numberOfBytes": "32" + }, + "t_struct(RoleData)9957_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "a126b9c885bba301b6802e2191f6360a6b7bdc5e260785f1037251a8d7ccb0d1": { + "address": "0x10Fad91a1E468CC887C4caBd084AFC4678a3f576", + "txHash": "0x444d4882871bead83b845b0909fa189fd0471214154958d2f275940832c90582", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(IMidasAccessControl)37005", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:31", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:36" + }, + { + "label": "contractPaused", + "offset": 0, + "slot": "51", + "type": "t_mapping(t_address,t_bool)", + "contract": "MidasPauseManager", + "src": "contracts/access/MidasPauseManager.sol:31" + }, + { + "label": "contractFnPaused", + "offset": 0, + "slot": "52", + "type": "t_mapping(t_address,t_mapping(t_bytes4,t_bool))", + "contract": "MidasPauseManager", + "src": "contracts/access/MidasPauseManager.sol:36" + }, + { + "label": "pauseDelay", + "offset": 0, + "slot": "53", + "type": "t_uint32", + "contract": "MidasPauseManager", + "src": "contracts/access/MidasPauseManager.sol:41" + }, + { + "label": "unpauseDelay", + "offset": 4, + "slot": "53", + "type": "t_uint32", + "contract": "MidasPauseManager", + "src": "contracts/access/MidasPauseManager.sol:46" + }, + { + "label": "globalPaused", + "offset": 8, + "slot": "53", + "type": "t_bool", + "contract": "MidasPauseManager", + "src": "contracts/access/MidasPauseManager.sol:51" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes4": { + "label": "bytes4", + "numberOfBytes": "4" + }, + "t_contract(IMidasAccessControl)37005": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_bytes4,t_bool))": { + "label": "mapping(address => mapping(bytes4 => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes4,t_bool)": { + "label": "mapping(bytes4 => bool)", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "e2315ea6241d2ea6edfd531a9cc8c7fcd75b525e334d1763756efa59fc902271": { + "address": "0xE4107C64A9ab2B2730de7c7035324bfE3993edC6", + "txHash": "0x8a17a1b65a6a12321badaf85686a7a05f5d47253ea15919432f9f6de9b8a2563", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(IMidasAccessControl)37005", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:31", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:36" + }, + { + "label": "_status", + "offset": 0, + "slot": "51", + "type": "t_uint256", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:38" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:88" + }, + { + "label": "dataHashIndexes", + "offset": 0, + "slot": "101", + "type": "t_mapping(t_bytes32,t_uint256)", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:84" + }, + { + "label": "proposerPendingOperationsCount", + "offset": 0, + "slot": "102", + "type": "t_mapping(t_address,t_uint256)", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:89" + }, + { + "label": "_securityCouncils", + "offset": 0, + "slot": "103", + "type": "t_mapping(t_uint256,t_struct(AddressSet)14890_storage)", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:94" + }, + { + "label": "_operationDetails", + "offset": 0, + "slot": "104", + "type": "t_mapping(t_bytes32,t_struct(TimelockOperationDetails)31632_storage)", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:99" + }, + { + "label": "_pendingOperations", + "offset": 0, + "slot": "105", + "type": "t_struct(Bytes32Set)14769_storage", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:104" + }, + { + "label": "timelock", + "offset": 0, + "slot": "107", + "type": "t_address", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:109" + }, + { + "label": "maxPendingOperationsPerProposer", + "offset": 0, + "slot": "108", + "type": "t_uint256", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:114" + }, + { + "label": "securityCouncilVersion", + "offset": 0, + "slot": "109", + "type": "t_uint256", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:119" + }, + { + "label": "pendingSetCouncilOperationId", + "offset": 0, + "slot": "110", + "type": "t_bytes32", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:124" + }, + { + "label": "__gap", + "offset": 0, + "slot": "111", + "type": "t_array(t_uint256)50_storage", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:129" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IMidasAccessControl)37005": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_enum(TimelockOperationStatus)37167": { + "label": "enum TimelockOperationStatus", + "members": [ + "NotExist", + "Pending", + "Paused", + "ApprovedExecution", + "ReadyToExecute", + "ReadyToAbort", + "Expired", + "Aborted", + "Executed" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(TimelockOperationDetails)31632_storage)": { + "label": "mapping(bytes32 => struct MidasTimelockManager.TimelockOperationDetails)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(AddressSet)14890_storage)": { + "label": "mapping(uint256 => struct EnumerableSetUpgradeable.AddressSet)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)14890_storage": { + "label": "struct EnumerableSetUpgradeable.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)14575_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Bytes32Set)14769_storage": { + "label": "struct EnumerableSetUpgradeable.Bytes32Set", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)14575_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Set)14575_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TimelockOperationDetails)31632_storage": { + "label": "struct MidasTimelockManager.TimelockOperationDetails", + "members": [ + { + "label": "votersForExecution", + "type": "t_struct(AddressSet)14890_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "votersForVeto", + "type": "t_struct(AddressSet)14890_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "councilVersion", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "dataHash", + "type": "t_bytes32", + "offset": 0, + "slot": "5" + }, + { + "label": "status", + "type": "t_enum(TimelockOperationStatus)37167", + "offset": 0, + "slot": "6" + }, + { + "label": "pauseReasonCode", + "type": "t_uint8", + "offset": 1, + "slot": "6" + }, + { + "label": "isSetCouncilOperation", + "type": "t_bool", + "offset": 2, + "slot": "6" + }, + { + "label": "createdAt", + "type": "t_uint32", + "offset": 3, + "slot": "6" + }, + { + "label": "executionApprovedAt", + "type": "t_uint32", + "offset": 7, + "slot": "6" + }, + { + "label": "operationProposer", + "type": "t_address", + "offset": 11, + "slot": "6" + }, + { + "label": "pauser", + "type": "t_address", + "offset": 0, + "slot": "7" + } + ], + "numberOfBytes": "256" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "7262a056736a11dbb5a53975a018c7e829bf0a9db69b5d4a7f2a9b382e23c660": { + "address": "0x7326b037332Cb248Eb007d643649F4d742A26eec", + "txHash": "0xae9704e9065c3a47b15f6ce7eff4b81a40bcbcbc487495a9e43b966e113388c4", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_roles", + "offset": 0, + "slot": "101", + "type": "t_mapping(t_bytes32,t_struct(RoleData)9957_storage)", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:62" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:260" + }, + { + "label": "_timestamps", + "offset": 0, + "slot": "151", + "type": "t_mapping(t_bytes32,t_uint256)", + "contract": "TimelockControllerUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol:33" + }, + { + "label": "_minDelay", + "offset": 0, + "slot": "152", + "type": "t_uint256", + "contract": "TimelockControllerUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol:34" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)48_storage", + "contract": "TimelockControllerUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol:433" + }, + { + "label": "timelockManager", + "offset": 0, + "slot": "201", + "type": "t_address", + "contract": "MidasAccessControlTimelockController", + "src": "contracts/access/MidasAccessControlTimelockController.sol:19" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "MidasAccessControlTimelockController", + "src": "contracts/access/MidasAccessControlTimelockController.sol:24" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)48_storage": { + "label": "uint256[48]", + "numberOfBytes": "1536" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)9957_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_struct(RoleData)9957_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "41882497f70e7e9c3e719a15270b2950e9a1da6a5e5dc447d681bf3dbee9d443": { + "address": "0x83BB73f653F93e3E0B52Ef323f93D767d5dB924F", + "txHash": "0x16daf1436d7134c942e6822e5a4cc0505569cca59835eccab90c8221c88b4a14", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(IMidasAccessControl)37005", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:31", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:36" + }, + { + "label": "description", + "offset": 0, + "slot": "51", + "type": "t_string_storage", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:40" + }, + { + "label": "latestRound", + "offset": 0, + "slot": "52", + "type": "t_uint80", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:45" + }, + { + "label": "maxAnswerDeviation", + "offset": 0, + "slot": "53", + "type": "t_uint256", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:51" + }, + { + "label": "minAnswer", + "offset": 0, + "slot": "54", + "type": "t_int192", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:56" + }, + { + "label": "maxAnswer", + "offset": 0, + "slot": "55", + "type": "t_int192", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:61" + }, + { + "label": "_roundData", + "offset": 0, + "slot": "56", + "type": "t_mapping(t_uint80,t_struct(RoundData)33952_storage)", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:66" + }, + { + "label": "__gap", + "offset": 0, + "slot": "57", + "type": "t_array(t_uint256)50_storage", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:71" + } + ], + "types": { + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IMidasAccessControl)37005": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_int192": { + "label": "int192", + "numberOfBytes": "24" + }, + "t_int256": { + "label": "int256", + "numberOfBytes": "32" + }, + "t_mapping(t_uint80,t_struct(RoundData)33952_storage)": { + "label": "mapping(uint80 => struct CustomAggregatorV3CompatibleFeed.RoundData)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(RoundData)33952_storage": { + "label": "struct CustomAggregatorV3CompatibleFeed.RoundData", + "members": [ + { + "label": "roundId", + "type": "t_uint80", + "offset": 0, + "slot": "0" + }, + { + "label": "answer", + "type": "t_int256", + "offset": 0, + "slot": "1" + }, + { + "label": "startedAt", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "updatedAt", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "answeredInRound", + "type": "t_uint80", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + }, + "t_uint80": { + "label": "uint80", + "numberOfBytes": "10" + } + } + } + }, + "dac7ae292c3bf8cfe0ea3840ff0efb17d72f65abad2854c40b40513843d9a54c": { + "address": "0xD30d95068E98683123e4e0F6CfA7562cc8aA8AA0", + "txHash": "0x3d0f23dfc31b27cc803d62fef382916fe3812c75cceb0c9616d2c1750e294b6f", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(IMidasAccessControl)37005", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:31", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:36" + }, + { + "label": "description", + "offset": 0, + "slot": "51", + "type": "t_string_storage", + "contract": "CustomAggregatorV3CompatibleFeedGrowth", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol:50" + }, + { + "label": "maxAnswerDeviation", + "offset": 0, + "slot": "52", + "type": "t_uint256", + "contract": "CustomAggregatorV3CompatibleFeedGrowth", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol:56" + }, + { + "label": "minAnswer", + "offset": 0, + "slot": "53", + "type": "t_int192", + "contract": "CustomAggregatorV3CompatibleFeedGrowth", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol:61" + }, + { + "label": "maxAnswer", + "offset": 0, + "slot": "54", + "type": "t_int192", + "contract": "CustomAggregatorV3CompatibleFeedGrowth", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol:66" + }, + { + "label": "minGrowthApr", + "offset": 0, + "slot": "55", + "type": "t_int80", + "contract": "CustomAggregatorV3CompatibleFeedGrowth", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol:71" + }, + { + "label": "maxGrowthApr", + "offset": 10, + "slot": "55", + "type": "t_int80", + "contract": "CustomAggregatorV3CompatibleFeedGrowth", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol:76" + }, + { + "label": "latestRound", + "offset": 20, + "slot": "55", + "type": "t_uint80", + "contract": "CustomAggregatorV3CompatibleFeedGrowth", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol:81" + }, + { + "label": "onlyUp", + "offset": 30, + "slot": "55", + "type": "t_bool", + "contract": "CustomAggregatorV3CompatibleFeedGrowth", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol:87" + }, + { + "label": "_roundData", + "offset": 0, + "slot": "56", + "type": "t_mapping(t_uint80,t_struct(RoundDataWithGrowth)34621_storage)", + "contract": "CustomAggregatorV3CompatibleFeedGrowth", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol:92" + }, + { + "label": "__gap", + "offset": 0, + "slot": "57", + "type": "t_array(t_uint256)50_storage", + "contract": "CustomAggregatorV3CompatibleFeedGrowth", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol:97" + }, + { + "label": "___gap", + "offset": 0, + "slot": "107", + "type": "t_array(t_uint256)50_storage", + "contract": "CustomAggregatorV3CompatibleFeedGrowth", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol:102" + } + ], + "types": { + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IMidasAccessControl)37005": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_int192": { + "label": "int192", + "numberOfBytes": "24" + }, + "t_int256": { + "label": "int256", + "numberOfBytes": "32" + }, + "t_int80": { + "label": "int80", + "numberOfBytes": "10" + }, + "t_mapping(t_uint80,t_struct(RoundDataWithGrowth)34621_storage)": { + "label": "mapping(uint80 => struct CustomAggregatorV3CompatibleFeedGrowth.RoundDataWithGrowth)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(RoundDataWithGrowth)34621_storage": { + "label": "struct CustomAggregatorV3CompatibleFeedGrowth.RoundDataWithGrowth", + "members": [ + { + "label": "roundId", + "type": "t_uint80", + "offset": 0, + "slot": "0" + }, + { + "label": "answeredInRound", + "type": "t_uint80", + "offset": 10, + "slot": "0" + }, + { + "label": "growthApr", + "type": "t_int80", + "offset": 20, + "slot": "0" + }, + { + "label": "answer", + "type": "t_int256", + "offset": 0, + "slot": "1" + }, + { + "label": "startedAt", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "updatedAt", + "type": "t_uint256", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + }, + "t_uint80": { + "label": "uint80", + "numberOfBytes": "10" + } + } + } + }, + "8c48189fe801cb8895506566da34c8343e43270f28c937c1cddcdf7cbae78543": { + "address": "0x88557BCbb69b676810ca951E8b5C84837A7C9a2f", + "txHash": "0x81b770a2726e89a2f6401a9eaf3d05b21869d905729b865b2eaece5b957f4368", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(IMidasAccessControl)37005", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:31", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:36" + }, + { + "label": "aggregator", + "offset": 0, + "slot": "51", + "type": "t_contract(AggregatorV3Interface)1801", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:31" + }, + { + "label": "healthyDiff", + "offset": 0, + "slot": "52", + "type": "t_uint256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:36" + }, + { + "label": "minExpectedAnswer", + "offset": 0, + "slot": "53", + "type": "t_int256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:41" + }, + { + "label": "maxExpectedAnswer", + "offset": 0, + "slot": "54", + "type": "t_int256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:46" + }, + { + "label": "__gap", + "offset": 0, + "slot": "55", + "type": "t_array(t_uint256)50_storage", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:51" + }, + { + "label": "___gap", + "offset": 0, + "slot": "105", + "type": "t_array(t_uint256)50_storage", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:56" + } + ], + "types": { + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(AggregatorV3Interface)1801": { + "label": "contract AggregatorV3Interface", + "numberOfBytes": "20" + }, + "t_contract(IMidasAccessControl)37005": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_int256": { + "label": "int256", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "d329f90349be99f0c1fc5cb2710494c305693139536df3f57185fb041907ef10": { + "address": "0x25d6bA13aF3742eb57D691107d4277a79812Ea71", + "txHash": "0x645125e0c43d89d7c9c1a0eb6bf8459dd880169adccfe5d162d53592ea796860", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_balances", + "offset": 0, + "slot": "51", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:40" + }, + { + "label": "_allowances", + "offset": 0, + "slot": "52", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:42" + }, + { + "label": "_totalSupply", + "offset": 0, + "slot": "53", + "type": "t_uint256", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:44" + }, + { + "label": "_name", + "offset": 0, + "slot": "54", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:46" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "55", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:47" + }, + { + "label": "__gap", + "offset": 0, + "slot": "56", + "type": "t_array(t_uint256)45_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:376" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC20PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol:48" + }, + { + "label": "accessControl", + "offset": 0, + "slot": "201", + "type": "t_contract(IMidasAccessControl)37005", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:31", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:17" + }, + { + "label": "metadata", + "offset": 0, + "slot": "302", + "type": "t_mapping(t_bytes32,t_bytes_storage)", + "contract": "mToken", + "src": "contracts/mToken.sol:48" + }, + { + "label": "_mintRateLimits", + "offset": 0, + "slot": "303", + "type": "t_struct(WindowRateLimits)39059_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:53" + }, + { + "label": "clawbackReceiver", + "offset": 0, + "slot": "306", + "type": "t_address", + "contract": "mToken", + "src": "contracts/mToken.sol:58" + }, + { + "label": "_inClawback", + "offset": 20, + "slot": "306", + "type": "t_bool", + "contract": "mToken", + "src": "contracts/mToken.sol:63" + }, + { + "label": "_name", + "offset": 0, + "slot": "307", + "type": "t_string_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:68" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "308", + "type": "t_string_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:73" + }, + { + "label": "__gap", + "offset": 0, + "slot": "309", + "type": "t_array(t_uint256)44_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:78" + }, + { + "label": "___gap", + "offset": 0, + "slot": "353", + "type": "t_array(t_uint256)50_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:83" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)44_storage": { + "label": "uint256[44]", + "numberOfBytes": "1408" + }, + "t_array(t_uint256)45_storage": { + "label": "uint256[45]", + "numberOfBytes": "1440" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_contract(IMidasAccessControl)37005": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bytes_storage)": { + "label": "mapping(bytes32 => bytes)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)39049_storage)": { + "label": "mapping(uint256 => struct RateLimitLibrary.WindowRateLimitConfig)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Set)14575_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(UintSet)15047_storage": { + "label": "struct EnumerableSetUpgradeable.UintSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)14575_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(WindowRateLimitConfig)39049_storage": { + "label": "struct RateLimitLibrary.WindowRateLimitConfig", + "members": [ + { + "label": "limit", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amountInFlight", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "window", + "type": "t_uint256", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(WindowRateLimits)39059_storage": { + "label": "struct RateLimitLibrary.WindowRateLimits", + "members": [ + { + "label": "windows", + "type": "t_struct(UintSet)15047_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "configs", + "type": "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)39049_storage)", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "ff1293c348820419429b1c45b4f5cec2c1d888d62a96eae2dda72edff6a201af": { + "address": "0xE9999c3D9869D0128F35a689E6ea19Cb12B328FA", + "txHash": "0x99945d87801306bf946fdee0a52dbe0d3c69c35a073fb9511c4300cc086ca095", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_balances", + "offset": 0, + "slot": "51", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:40" + }, + { + "label": "_allowances", + "offset": 0, + "slot": "52", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:42" + }, + { + "label": "_totalSupply", + "offset": 0, + "slot": "53", + "type": "t_uint256", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:44" + }, + { + "label": "_name", + "offset": 0, + "slot": "54", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:46" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "55", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:47" + }, + { + "label": "__gap", + "offset": 0, + "slot": "56", + "type": "t_array(t_uint256)45_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:376" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC20PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol:48" + }, + { + "label": "accessControl", + "offset": 0, + "slot": "201", + "type": "t_contract(IMidasAccessControl)37005", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:31", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:17" + }, + { + "label": "metadata", + "offset": 0, + "slot": "302", + "type": "t_mapping(t_bytes32,t_bytes_storage)", + "contract": "mToken", + "src": "contracts/mToken.sol:48" + }, + { + "label": "_mintRateLimits", + "offset": 0, + "slot": "303", + "type": "t_struct(WindowRateLimits)39059_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:53" + }, + { + "label": "clawbackReceiver", + "offset": 0, + "slot": "306", + "type": "t_address", + "contract": "mToken", + "src": "contracts/mToken.sol:58" + }, + { + "label": "_inClawback", + "offset": 20, + "slot": "306", + "type": "t_bool", + "contract": "mToken", + "src": "contracts/mToken.sol:63" + }, + { + "label": "_name", + "offset": 0, + "slot": "307", + "type": "t_string_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:68" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "308", + "type": "t_string_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:73" + }, + { + "label": "__gap", + "offset": 0, + "slot": "309", + "type": "t_array(t_uint256)44_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:78" + }, + { + "label": "___gap", + "offset": 0, + "slot": "353", + "type": "t_array(t_uint256)50_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:83" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)44_storage": { + "label": "uint256[44]", + "numberOfBytes": "1408" + }, + "t_array(t_uint256)45_storage": { + "label": "uint256[45]", + "numberOfBytes": "1440" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_contract(IMidasAccessControl)37005": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bytes_storage)": { + "label": "mapping(bytes32 => bytes)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)39049_storage)": { + "label": "mapping(uint256 => struct RateLimitLibrary.WindowRateLimitConfig)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Set)14575_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(UintSet)15047_storage": { + "label": "struct EnumerableSetUpgradeable.UintSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)14575_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(WindowRateLimitConfig)39049_storage": { + "label": "struct RateLimitLibrary.WindowRateLimitConfig", + "members": [ + { + "label": "limit", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amountInFlight", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "window", + "type": "t_uint256", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(WindowRateLimits)39059_storage": { + "label": "struct RateLimitLibrary.WindowRateLimits", + "members": [ + { + "label": "windows", + "type": "t_struct(UintSet)15047_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "configs", + "type": "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)39049_storage)", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "9212f7c0d6393cbf9f66f44dacee270a8914f2d880b22005c76b4244e8db1243": { + "address": "0xa8988ea6B4a94A15e8185215f49a32c9e5E12C5F", + "txHash": "0xc0eeff356cd73b771f6bf4dc9e35cf9a546f5bf8d2cc05d1b8dff24ad9338e9a", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(IMidasAccessControl)37005", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:31", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:36" + }, + { + "label": "aggregator", + "offset": 0, + "slot": "51", + "type": "t_contract(AggregatorV3Interface)1801", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:31" + }, + { + "label": "healthyDiff", + "offset": 0, + "slot": "52", + "type": "t_uint256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:36" + }, + { + "label": "minExpectedAnswer", + "offset": 0, + "slot": "53", + "type": "t_int256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:41" + }, + { + "label": "maxExpectedAnswer", + "offset": 0, + "slot": "54", + "type": "t_int256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:46" + }, + { + "label": "__gap", + "offset": 0, + "slot": "55", + "type": "t_array(t_uint256)50_storage", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:51" + }, + { + "label": "___gap", + "offset": 0, + "slot": "105", + "type": "t_array(t_uint256)50_storage", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:56" + } + ], + "types": { + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(AggregatorV3Interface)1801": { + "label": "contract AggregatorV3Interface", + "numberOfBytes": "20" + }, + "t_contract(IMidasAccessControl)37005": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_int256": { + "label": "int256", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "e1ac3fcb29bba83c7285d7f6dd1712aade9f91a31013b930c960a972d4bb01d4": { + "address": "0x5FfDE2871aC76fc033b3F775e280840a5600Cd16", + "txHash": "0x093401621115c5472968f287aa1b7056f7d10cd2fff8f031818cc3d58a4d8fa2", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(IMidasAccessControl)13426", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:31", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:17" + }, + { + "label": "greenlistEnabled", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:17" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)50_storage", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:22" + }, + { + "label": "sanctionsList", + "offset": 0, + "slot": "152", + "type": "t_address", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:17" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)50_storage", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:22" + }, + { + "label": "tokensConfig", + "offset": 0, + "slot": "203", + "type": "t_mapping(t_address,t_struct(TokenConfig)12675_storage)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:73" + }, + { + "label": "isFreeFromMinAmount", + "offset": 0, + "slot": "204", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:78" + }, + { + "label": "waivedFeeRestriction", + "offset": 0, + "slot": "205", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:83" + }, + { + "label": "_paymentTokens", + "offset": 0, + "slot": "206", + "type": "t_struct(AddressSet)2278_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:88" + }, + { + "label": "_instantRateLimits", + "offset": 0, + "slot": "208", + "type": "t_struct(WindowRateLimits)15297_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:93" + }, + { + "label": "currentRequestId", + "offset": 0, + "slot": "211", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:98" + }, + { + "label": "nextExpectedRequestIdToProcess", + "offset": 0, + "slot": "212", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:103" + }, + { + "label": "maxApproveRequestId", + "offset": 0, + "slot": "213", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:108" + }, + { + "label": "mToken", + "offset": 0, + "slot": "214", + "type": "t_contract(IMToken)12657", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:113" + }, + { + "label": "mTokenDataFeed", + "offset": 0, + "slot": "215", + "type": "t_contract(IDataFeed)12284", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:118" + }, + { + "label": "tokensReceiver", + "offset": 0, + "slot": "216", + "type": "t_address", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:123" + }, + { + "label": "variationTolerance", + "offset": 0, + "slot": "217", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:128" + }, + { + "label": "minAmount", + "offset": 0, + "slot": "218", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:133" + }, + { + "label": "instantFee", + "offset": 0, + "slot": "219", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:138" + }, + { + "label": "minInstantFee", + "offset": 0, + "slot": "220", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:143" + }, + { + "label": "maxInstantFee", + "offset": 0, + "slot": "221", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:148" + }, + { + "label": "maxInstantShare", + "offset": 0, + "slot": "222", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:153" + }, + { + "label": "sequentialRequestProcessing", + "offset": 0, + "slot": "223", + "type": "t_bool", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:158" + }, + { + "label": "__gap", + "offset": 0, + "slot": "224", + "type": "t_array(t_uint256)50_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:163" + }, + { + "label": "mintRequests", + "offset": 0, + "slot": "274", + "type": "t_mapping(t_uint256,t_struct(Request)12317_storage)", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:44" + }, + { + "label": "totalMinted", + "offset": 0, + "slot": "275", + "type": "t_mapping(t_address,t_uint256)", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:50" + }, + { + "label": "minMTokenAmountForFirstDeposit", + "offset": 0, + "slot": "276", + "type": "t_uint256", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:55" + }, + { + "label": "maxSupplyCap", + "offset": 0, + "slot": "277", + "type": "t_uint256", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:62" + }, + { + "label": "maxAmountPerRequest", + "offset": 0, + "slot": "278", + "type": "t_uint256", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:67" + }, + { + "label": "upcomingSupply", + "offset": 0, + "slot": "279", + "type": "t_uint256", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:73" + }, + { + "label": "__gap", + "offset": 0, + "slot": "280", + "type": "t_array(t_uint256)50_storage", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:78" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IDataFeed)12284": { + "label": "contract IDataFeed", + "numberOfBytes": "20" + }, + "t_contract(IMToken)12657": { + "label": "contract IMToken", + "numberOfBytes": "20" + }, + "t_contract(IMidasAccessControl)13426": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_enum(RequestStatus)12679": { + "label": "enum RequestStatus", + "members": ["Pending", "Processed", "Canceled"], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(TokenConfig)12675_storage)": { + "label": "mapping(address => struct TokenConfig)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Request)12317_storage)": { + "label": "mapping(uint256 => struct Request)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)15287_storage)": { + "label": "mapping(uint256 => struct RateLimitLibrary.WindowRateLimitConfig)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)2278_storage": { + "label": "struct EnumerableSetUpgradeable.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)1963_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Request)12317_storage": { + "label": "struct Request", + "members": [ + { + "label": "recipient", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "tokenIn", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "status", + "type": "t_enum(RequestStatus)12679", + "offset": 20, + "slot": "1" + }, + { + "label": "depositedUsdAmount", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "usdAmountWithoutFees", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "tokenOutRate", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "depositedInstantUsdAmount", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "approvedTokenOutRate", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "amountMToken", + "type": "t_uint256", + "offset": 0, + "slot": "7" + } + ], + "numberOfBytes": "256" + }, + "t_struct(Set)1963_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TokenConfig)12675_storage": { + "label": "struct TokenConfig", + "members": [ + { + "label": "dataFeed", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "fee", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "allowance", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "stable", + "type": "t_bool", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(UintSet)2435_storage": { + "label": "struct EnumerableSetUpgradeable.UintSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)1963_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(WindowRateLimitConfig)15287_storage": { + "label": "struct RateLimitLibrary.WindowRateLimitConfig", + "members": [ + { + "label": "limit", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amountInFlight", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "window", + "type": "t_uint256", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(WindowRateLimits)15297_storage": { + "label": "struct RateLimitLibrary.WindowRateLimits", + "members": [ + { + "label": "windows", + "type": "t_struct(UintSet)2435_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "configs", + "type": "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)15287_storage)", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "46731beb42684086b70c655a4821f81a82d510642b2bc658a03f4aaf684bfdbb": { + "address": "0xC3512A9bcdE459607C8599a34E3B834f9fCF235b", + "txHash": "0xc033934264b71662cfbaa821661b258cf27519ecd6e1e586371b958f1c09be75", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(IMidasAccessControl)13426", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:31", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:17" + }, + { + "label": "greenlistEnabled", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:17" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)50_storage", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:22" + }, + { + "label": "sanctionsList", + "offset": 0, + "slot": "152", + "type": "t_address", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:17" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)50_storage", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:22" + }, + { + "label": "tokensConfig", + "offset": 0, + "slot": "203", + "type": "t_mapping(t_address,t_struct(TokenConfig)12675_storage)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:73" + }, + { + "label": "isFreeFromMinAmount", + "offset": 0, + "slot": "204", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:78" + }, + { + "label": "waivedFeeRestriction", + "offset": 0, + "slot": "205", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:83" + }, + { + "label": "_paymentTokens", + "offset": 0, + "slot": "206", + "type": "t_struct(AddressSet)2278_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:88" + }, + { + "label": "_instantRateLimits", + "offset": 0, + "slot": "208", + "type": "t_struct(WindowRateLimits)15297_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:93" + }, + { + "label": "currentRequestId", + "offset": 0, + "slot": "211", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:98" + }, + { + "label": "nextExpectedRequestIdToProcess", + "offset": 0, + "slot": "212", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:103" + }, + { + "label": "maxApproveRequestId", + "offset": 0, + "slot": "213", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:108" + }, + { + "label": "mToken", + "offset": 0, + "slot": "214", + "type": "t_contract(IMToken)12657", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:113" + }, + { + "label": "mTokenDataFeed", + "offset": 0, + "slot": "215", + "type": "t_contract(IDataFeed)12284", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:118" + }, + { + "label": "tokensReceiver", + "offset": 0, + "slot": "216", + "type": "t_address", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:123" + }, + { + "label": "variationTolerance", + "offset": 0, + "slot": "217", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:128" + }, + { + "label": "minAmount", + "offset": 0, + "slot": "218", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:133" + }, + { + "label": "instantFee", + "offset": 0, + "slot": "219", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:138" + }, + { + "label": "minInstantFee", + "offset": 0, + "slot": "220", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:143" + }, + { + "label": "maxInstantFee", + "offset": 0, + "slot": "221", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:148" + }, + { + "label": "maxInstantShare", + "offset": 0, + "slot": "222", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:153" + }, + { + "label": "sequentialRequestProcessing", + "offset": 0, + "slot": "223", + "type": "t_bool", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:158" + }, + { + "label": "__gap", + "offset": 0, + "slot": "224", + "type": "t_array(t_uint256)50_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:163" + }, + { + "label": "mintRequests", + "offset": 0, + "slot": "274", + "type": "t_mapping(t_uint256,t_struct(Request)12317_storage)", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:44" + }, + { + "label": "totalMinted", + "offset": 0, + "slot": "275", + "type": "t_mapping(t_address,t_uint256)", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:50" + }, + { + "label": "minMTokenAmountForFirstDeposit", + "offset": 0, + "slot": "276", + "type": "t_uint256", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:55" + }, + { + "label": "maxSupplyCap", + "offset": 0, + "slot": "277", + "type": "t_uint256", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:62" + }, + { + "label": "maxAmountPerRequest", + "offset": 0, + "slot": "278", + "type": "t_uint256", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:67" + }, + { + "label": "upcomingSupply", + "offset": 0, + "slot": "279", + "type": "t_uint256", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:73" + }, + { + "label": "__gap", + "offset": 0, + "slot": "280", + "type": "t_array(t_uint256)50_storage", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:78" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IDataFeed)12284": { + "label": "contract IDataFeed", + "numberOfBytes": "20" + }, + "t_contract(IMToken)12657": { + "label": "contract IMToken", + "numberOfBytes": "20" + }, + "t_contract(IMidasAccessControl)13426": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_enum(RequestStatus)12679": { + "label": "enum RequestStatus", + "members": ["Pending", "Processed", "Canceled"], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(TokenConfig)12675_storage)": { + "label": "mapping(address => struct TokenConfig)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Request)12317_storage)": { + "label": "mapping(uint256 => struct Request)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)15287_storage)": { + "label": "mapping(uint256 => struct RateLimitLibrary.WindowRateLimitConfig)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)2278_storage": { + "label": "struct EnumerableSetUpgradeable.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)1963_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Request)12317_storage": { + "label": "struct Request", + "members": [ + { + "label": "recipient", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "tokenIn", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "status", + "type": "t_enum(RequestStatus)12679", + "offset": 20, + "slot": "1" + }, + { + "label": "depositedUsdAmount", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "usdAmountWithoutFees", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "tokenOutRate", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "depositedInstantUsdAmount", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "approvedTokenOutRate", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "amountMToken", + "type": "t_uint256", + "offset": 0, + "slot": "7" + } + ], + "numberOfBytes": "256" + }, + "t_struct(Set)1963_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TokenConfig)12675_storage": { + "label": "struct TokenConfig", + "members": [ + { + "label": "dataFeed", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "fee", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "allowance", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "stable", + "type": "t_bool", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(UintSet)2435_storage": { + "label": "struct EnumerableSetUpgradeable.UintSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)1963_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(WindowRateLimitConfig)15287_storage": { + "label": "struct RateLimitLibrary.WindowRateLimitConfig", + "members": [ + { + "label": "limit", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amountInFlight", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "window", + "type": "t_uint256", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(WindowRateLimits)15297_storage": { + "label": "struct RateLimitLibrary.WindowRateLimits", + "members": [ + { + "label": "windows", + "type": "t_struct(UintSet)2435_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "configs", + "type": "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)15287_storage)", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "7c63d5b9dc0375dc28bad37ede49a85021fb80a7a13b0cdd8b3ad72be912cc5a": { + "address": "0xF17851FdF529Ab55864bD513EE00607D894868FF", + "txHash": "0xcff5131ced5d93ba06cd59b90487cf1f92cbfbc21a50b1348fb470bf2600ba48", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(IMidasAccessControl)13426", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:31", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:17" + }, + { + "label": "greenlistEnabled", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:17" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)50_storage", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:22" + }, + { + "label": "sanctionsList", + "offset": 0, + "slot": "152", + "type": "t_address", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:17" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)50_storage", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:22" + }, + { + "label": "tokensConfig", + "offset": 0, + "slot": "203", + "type": "t_mapping(t_address,t_struct(TokenConfig)12675_storage)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:73" + }, + { + "label": "isFreeFromMinAmount", + "offset": 0, + "slot": "204", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:78" + }, + { + "label": "waivedFeeRestriction", + "offset": 0, + "slot": "205", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:83" + }, + { + "label": "_paymentTokens", + "offset": 0, + "slot": "206", + "type": "t_struct(AddressSet)2278_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:88" + }, + { + "label": "_instantRateLimits", + "offset": 0, + "slot": "208", + "type": "t_struct(WindowRateLimits)15297_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:93" + }, + { + "label": "currentRequestId", + "offset": 0, + "slot": "211", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:98" + }, + { + "label": "nextExpectedRequestIdToProcess", + "offset": 0, + "slot": "212", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:103" + }, + { + "label": "maxApproveRequestId", + "offset": 0, + "slot": "213", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:108" + }, + { + "label": "mToken", + "offset": 0, + "slot": "214", + "type": "t_contract(IMToken)12657", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:113" + }, + { + "label": "mTokenDataFeed", + "offset": 0, + "slot": "215", + "type": "t_contract(IDataFeed)12284", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:118" + }, + { + "label": "tokensReceiver", + "offset": 0, + "slot": "216", + "type": "t_address", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:123" + }, + { + "label": "variationTolerance", + "offset": 0, + "slot": "217", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:128" + }, + { + "label": "minAmount", + "offset": 0, + "slot": "218", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:133" + }, + { + "label": "instantFee", + "offset": 0, + "slot": "219", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:138" + }, + { + "label": "minInstantFee", + "offset": 0, + "slot": "220", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:143" + }, + { + "label": "maxInstantFee", + "offset": 0, + "slot": "221", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:148" + }, + { + "label": "maxInstantShare", + "offset": 0, + "slot": "222", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:153" + }, + { + "label": "sequentialRequestProcessing", + "offset": 0, + "slot": "223", + "type": "t_bool", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:158" + }, + { + "label": "__gap", + "offset": 0, + "slot": "224", + "type": "t_array(t_uint256)50_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:163" + }, + { + "label": "redeemRequests", + "offset": 0, + "slot": "274", + "type": "t_mapping(t_uint256,t_struct(Request)14010_storage)", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:44" + }, + { + "label": "loanRequests", + "offset": 0, + "slot": "275", + "type": "t_mapping(t_uint256,t_struct(LiquidityProviderLoanRequest)14045_storage)", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:49" + }, + { + "label": "requestRedeemer", + "offset": 0, + "slot": "276", + "type": "t_address", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:54" + }, + { + "label": "loanLp", + "offset": 0, + "slot": "277", + "type": "t_address", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:59" + }, + { + "label": "loanRepaymentAddress", + "offset": 0, + "slot": "278", + "type": "t_address", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:64" + }, + { + "label": "loanApr", + "offset": 0, + "slot": "279", + "type": "t_uint256", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:69" + }, + { + "label": "preferLoanLiquidity", + "offset": 0, + "slot": "280", + "type": "t_bool", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:74" + }, + { + "label": "currentLoanRequestId", + "offset": 0, + "slot": "281", + "type": "t_uint256", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:79" + }, + { + "label": "loanSwapperVault", + "offset": 0, + "slot": "282", + "type": "t_contract(IRedemptionVault)14332", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:84" + }, + { + "label": "__gap", + "offset": 0, + "slot": "283", + "type": "t_array(t_uint256)50_storage", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:89" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IDataFeed)12284": { + "label": "contract IDataFeed", + "numberOfBytes": "20" + }, + "t_contract(IMToken)12657": { + "label": "contract IMToken", + "numberOfBytes": "20" + }, + "t_contract(IMidasAccessControl)13426": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_contract(IRedemptionVault)14332": { + "label": "contract IRedemptionVault", + "numberOfBytes": "20" + }, + "t_enum(RequestStatus)12679": { + "label": "enum RequestStatus", + "members": ["Pending", "Processed", "Canceled"], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(TokenConfig)12675_storage)": { + "label": "mapping(address => struct TokenConfig)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(LiquidityProviderLoanRequest)14045_storage)": { + "label": "mapping(uint256 => struct LiquidityProviderLoanRequest)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Request)14010_storage)": { + "label": "mapping(uint256 => struct Request)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)15287_storage)": { + "label": "mapping(uint256 => struct RateLimitLibrary.WindowRateLimitConfig)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)2278_storage": { + "label": "struct EnumerableSetUpgradeable.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)1963_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(LiquidityProviderLoanRequest)14045_storage": { + "label": "struct LiquidityProviderLoanRequest", + "members": [ + { + "label": "tokenOut", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "amountTokenOut", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "amountFee", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "createdAt", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "status", + "type": "t_enum(RequestStatus)12679", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_struct(Request)14010_storage": { + "label": "struct Request", + "members": [ + { + "label": "recipient", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "tokenOut", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "status", + "type": "t_enum(RequestStatus)12679", + "offset": 20, + "slot": "1" + }, + { + "label": "feePercent", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "amountMToken", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "mTokenRate", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "tokenOutRate", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "amountMTokenInstant", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "approvedMTokenRate", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "amountTokenOut", + "type": "t_uint256", + "offset": 0, + "slot": "8" + } + ], + "numberOfBytes": "288" + }, + "t_struct(Set)1963_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TokenConfig)12675_storage": { + "label": "struct TokenConfig", + "members": [ + { + "label": "dataFeed", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "fee", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "allowance", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "stable", + "type": "t_bool", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(UintSet)2435_storage": { + "label": "struct EnumerableSetUpgradeable.UintSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)1963_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(WindowRateLimitConfig)15287_storage": { + "label": "struct RateLimitLibrary.WindowRateLimitConfig", + "members": [ + { + "label": "limit", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amountInFlight", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "window", + "type": "t_uint256", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(WindowRateLimits)15297_storage": { + "label": "struct RateLimitLibrary.WindowRateLimits", + "members": [ + { + "label": "windows", + "type": "t_struct(UintSet)2435_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "configs", + "type": "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)15287_storage)", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "55f2a882774d1b505010be526407522659ffdfc4881e7816fd925fb298250dcf": { + "address": "0x26d6D401982B8d60004B313eE9381A98d4ce9E76", + "txHash": "0x4c0c0380eda2cfd4d71e541f1ab561e03e52b01a41faf22ffcbade37b5d74f82", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(IMidasAccessControl)13426", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:31", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:17" + }, + { + "label": "greenlistEnabled", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:17" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)50_storage", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:22" + }, + { + "label": "sanctionsList", + "offset": 0, + "slot": "152", + "type": "t_address", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:17" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)50_storage", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:22" + }, + { + "label": "tokensConfig", + "offset": 0, + "slot": "203", + "type": "t_mapping(t_address,t_struct(TokenConfig)12675_storage)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:73" + }, + { + "label": "isFreeFromMinAmount", + "offset": 0, + "slot": "204", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:78" + }, + { + "label": "waivedFeeRestriction", + "offset": 0, + "slot": "205", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:83" + }, + { + "label": "_paymentTokens", + "offset": 0, + "slot": "206", + "type": "t_struct(AddressSet)2278_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:88" + }, + { + "label": "_instantRateLimits", + "offset": 0, + "slot": "208", + "type": "t_struct(WindowRateLimits)15297_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:93" + }, + { + "label": "currentRequestId", + "offset": 0, + "slot": "211", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:98" + }, + { + "label": "nextExpectedRequestIdToProcess", + "offset": 0, + "slot": "212", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:103" + }, + { + "label": "maxApproveRequestId", + "offset": 0, + "slot": "213", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:108" + }, + { + "label": "mToken", + "offset": 0, + "slot": "214", + "type": "t_contract(IMToken)12657", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:113" + }, + { + "label": "mTokenDataFeed", + "offset": 0, + "slot": "215", + "type": "t_contract(IDataFeed)12284", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:118" + }, + { + "label": "tokensReceiver", + "offset": 0, + "slot": "216", + "type": "t_address", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:123" + }, + { + "label": "variationTolerance", + "offset": 0, + "slot": "217", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:128" + }, + { + "label": "minAmount", + "offset": 0, + "slot": "218", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:133" + }, + { + "label": "instantFee", + "offset": 0, + "slot": "219", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:138" + }, + { + "label": "minInstantFee", + "offset": 0, + "slot": "220", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:143" + }, + { + "label": "maxInstantFee", + "offset": 0, + "slot": "221", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:148" + }, + { + "label": "maxInstantShare", + "offset": 0, + "slot": "222", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:153" + }, + { + "label": "sequentialRequestProcessing", + "offset": 0, + "slot": "223", + "type": "t_bool", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:158" + }, + { + "label": "__gap", + "offset": 0, + "slot": "224", + "type": "t_array(t_uint256)50_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:163" + }, + { + "label": "redeemRequests", + "offset": 0, + "slot": "274", + "type": "t_mapping(t_uint256,t_struct(Request)14010_storage)", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:44" + }, + { + "label": "loanRequests", + "offset": 0, + "slot": "275", + "type": "t_mapping(t_uint256,t_struct(LiquidityProviderLoanRequest)14045_storage)", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:49" + }, + { + "label": "requestRedeemer", + "offset": 0, + "slot": "276", + "type": "t_address", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:54" + }, + { + "label": "loanLp", + "offset": 0, + "slot": "277", + "type": "t_address", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:59" + }, + { + "label": "loanRepaymentAddress", + "offset": 0, + "slot": "278", + "type": "t_address", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:64" + }, + { + "label": "loanApr", + "offset": 0, + "slot": "279", + "type": "t_uint256", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:69" + }, + { + "label": "preferLoanLiquidity", + "offset": 0, + "slot": "280", + "type": "t_bool", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:74" + }, + { + "label": "currentLoanRequestId", + "offset": 0, + "slot": "281", + "type": "t_uint256", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:79" + }, + { + "label": "loanSwapperVault", + "offset": 0, + "slot": "282", + "type": "t_contract(IRedemptionVault)14332", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:84" + }, + { + "label": "__gap", + "offset": 0, + "slot": "283", + "type": "t_array(t_uint256)50_storage", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:89" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IDataFeed)12284": { + "label": "contract IDataFeed", + "numberOfBytes": "20" + }, + "t_contract(IMToken)12657": { + "label": "contract IMToken", + "numberOfBytes": "20" + }, + "t_contract(IMidasAccessControl)13426": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_contract(IRedemptionVault)14332": { + "label": "contract IRedemptionVault", + "numberOfBytes": "20" + }, + "t_enum(RequestStatus)12679": { + "label": "enum RequestStatus", + "members": ["Pending", "Processed", "Canceled"], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(TokenConfig)12675_storage)": { + "label": "mapping(address => struct TokenConfig)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(LiquidityProviderLoanRequest)14045_storage)": { + "label": "mapping(uint256 => struct LiquidityProviderLoanRequest)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Request)14010_storage)": { + "label": "mapping(uint256 => struct Request)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)15287_storage)": { + "label": "mapping(uint256 => struct RateLimitLibrary.WindowRateLimitConfig)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)2278_storage": { + "label": "struct EnumerableSetUpgradeable.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)1963_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(LiquidityProviderLoanRequest)14045_storage": { + "label": "struct LiquidityProviderLoanRequest", + "members": [ + { + "label": "tokenOut", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "amountTokenOut", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "amountFee", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "createdAt", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "status", + "type": "t_enum(RequestStatus)12679", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_struct(Request)14010_storage": { + "label": "struct Request", + "members": [ + { + "label": "recipient", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "tokenOut", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "status", + "type": "t_enum(RequestStatus)12679", + "offset": 20, + "slot": "1" + }, + { + "label": "feePercent", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "amountMToken", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "mTokenRate", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "tokenOutRate", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "amountMTokenInstant", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "approvedMTokenRate", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "amountTokenOut", + "type": "t_uint256", + "offset": 0, + "slot": "8" + } + ], + "numberOfBytes": "288" + }, + "t_struct(Set)1963_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TokenConfig)12675_storage": { + "label": "struct TokenConfig", + "members": [ + { + "label": "dataFeed", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "fee", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "allowance", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "stable", + "type": "t_bool", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(UintSet)2435_storage": { + "label": "struct EnumerableSetUpgradeable.UintSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)1963_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(WindowRateLimitConfig)15287_storage": { + "label": "struct RateLimitLibrary.WindowRateLimitConfig", + "members": [ + { + "label": "limit", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amountInFlight", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "window", + "type": "t_uint256", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(WindowRateLimits)15297_storage": { + "label": "struct RateLimitLibrary.WindowRateLimits", + "members": [ + { + "label": "windows", + "type": "t_struct(UintSet)2435_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "configs", + "type": "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)15287_storage)", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "f879537ca17d83db8cdf95ab8f8e98f5476d47aa4308e955af8d9a7f58612397": { + "address": "0x0fB85F6Ce3BfBcf0B6eeA3d8edeDDe79d672EEB6", + "txHash": "0x5123db951769a70cb9625cd7e5215cc67901574738871c47e227d9679dc0c89b", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(IMidasAccessControl)37005", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:31", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:36" + }, + { + "label": "aggregator", + "offset": 0, + "slot": "51", + "type": "t_contract(AggregatorV3Interface)1801", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:31" + }, + { + "label": "healthyDiff", + "offset": 0, + "slot": "52", + "type": "t_uint256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:36" + }, + { + "label": "minExpectedAnswer", + "offset": 0, + "slot": "53", + "type": "t_int256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:41" + }, + { + "label": "maxExpectedAnswer", + "offset": 0, + "slot": "54", + "type": "t_int256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:46" + }, + { + "label": "__gap", + "offset": 0, + "slot": "55", + "type": "t_array(t_uint256)50_storage", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:51" + }, + { + "label": "___gap", + "offset": 0, + "slot": "105", + "type": "t_array(t_uint256)50_storage", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:56" + } + ], + "types": { + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(AggregatorV3Interface)1801": { + "label": "contract AggregatorV3Interface", + "numberOfBytes": "20" + }, + "t_contract(IMidasAccessControl)37005": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_int256": { + "label": "int256", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "66b47cbe6d29772f8f04362972f20aaa003fae06fcb09071059795b4f94cb61f": { + "address": "0xEcD9cCBC0eaC2377ea44606487E4235Cb256f658", + "txHash": "0x78c6778f00719c3db56b749120c9127f23d7758981acda838a93ed09d727e22a", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(IMidasAccessControl)22015", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:34" + }, + { + "label": "_status", + "offset": 0, + "slot": "51", + "type": "t_uint256", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:38" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:88" + }, + { + "label": "dataHashIndexes", + "offset": 0, + "slot": "101", + "type": "t_mapping(t_bytes32,t_uint256)", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:84" + }, + { + "label": "proposerPendingOperationsCount", + "offset": 0, + "slot": "102", + "type": "t_mapping(t_address,t_uint256)", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:89" + }, + { + "label": "_securityCouncils", + "offset": 0, + "slot": "103", + "type": "t_mapping(t_uint256,t_struct(AddressSet)5013_storage)", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:94" + }, + { + "label": "_operationDetails", + "offset": 0, + "slot": "104", + "type": "t_mapping(t_bytes32,t_struct(TimelockOperationDetails)16856_storage)", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:99" + }, + { + "label": "_pendingOperations", + "offset": 0, + "slot": "105", + "type": "t_struct(Bytes32Set)4892_storage", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:104" + }, + { + "label": "timelock", + "offset": 0, + "slot": "107", + "type": "t_address", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:109" + }, + { + "label": "maxPendingOperationsPerProposer", + "offset": 0, + "slot": "108", + "type": "t_uint256", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:114" + }, + { + "label": "securityCouncilVersion", + "offset": 0, + "slot": "109", + "type": "t_uint256", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:119" + }, + { + "label": "pendingSetCouncilOperationId", + "offset": 0, + "slot": "110", + "type": "t_bytes32", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:124" + }, + { + "label": "__gap", + "offset": 0, + "slot": "111", + "type": "t_array(t_uint256)50_storage", + "contract": "MidasTimelockManager", + "src": "contracts/access/MidasTimelockManager.sol:129" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IMidasAccessControl)22015": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_enum(TimelockOperationStatus)22177": { + "label": "enum TimelockOperationStatus", + "members": [ + "NotExist", + "Pending", + "Paused", + "ApprovedExecution", + "ReadyToExecute", + "ReadyToAbort", + "Expired", + "Aborted", + "Executed" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(TimelockOperationDetails)16856_storage)": { + "label": "mapping(bytes32 => struct MidasTimelockManager.TimelockOperationDetails)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(AddressSet)5013_storage)": { + "label": "mapping(uint256 => struct EnumerableSetUpgradeable.AddressSet)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)5013_storage": { + "label": "struct EnumerableSetUpgradeable.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)4698_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Bytes32Set)4892_storage": { + "label": "struct EnumerableSetUpgradeable.Bytes32Set", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)4698_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Set)4698_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TimelockOperationDetails)16856_storage": { + "label": "struct MidasTimelockManager.TimelockOperationDetails", + "members": [ + { + "label": "votersForExecution", + "type": "t_struct(AddressSet)5013_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "votersForVeto", + "type": "t_struct(AddressSet)5013_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "councilVersion", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "dataHash", + "type": "t_bytes32", + "offset": 0, + "slot": "5" + }, + { + "label": "status", + "type": "t_enum(TimelockOperationStatus)22177", + "offset": 0, + "slot": "6" + }, + { + "label": "pauseReasonCode", + "type": "t_uint8", + "offset": 1, + "slot": "6" + }, + { + "label": "isSetCouncilOperation", + "type": "t_bool", + "offset": 2, + "slot": "6" + }, + { + "label": "createdAt", + "type": "t_uint32", + "offset": 3, + "slot": "6" + }, + { + "label": "executionApprovedAt", + "type": "t_uint32", + "offset": 7, + "slot": "6" + }, + { + "label": "operationProposer", + "type": "t_address", + "offset": 11, + "slot": "6" + }, + { + "label": "pauser", + "type": "t_address", + "offset": 0, + "slot": "7" + } + ], + "numberOfBytes": "256" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "dd58e5cb4bfe6ab8fe1683a1ab110d099190acb166ae597a147b812398a642e0": { + "address": "0xc9d7Ee788f941905C86AB055C8366615af866837", + "txHash": "0x9ecc59e5995abaed70345e446e02dd082b61afdfb03372b118603acf37859f7b", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(IMidasAccessControl)22015", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:34" + }, + { + "label": "contractPaused", + "offset": 0, + "slot": "51", + "type": "t_mapping(t_address,t_bool)", + "contract": "MidasPauseManager", + "src": "contracts/access/MidasPauseManager.sol:31" + }, + { + "label": "contractFnPaused", + "offset": 0, + "slot": "52", + "type": "t_mapping(t_address,t_mapping(t_bytes4,t_bool))", + "contract": "MidasPauseManager", + "src": "contracts/access/MidasPauseManager.sol:36" + }, + { + "label": "pauseDelay", + "offset": 0, + "slot": "53", + "type": "t_uint32", + "contract": "MidasPauseManager", + "src": "contracts/access/MidasPauseManager.sol:41" + }, + { + "label": "unpauseDelay", + "offset": 4, + "slot": "53", + "type": "t_uint32", + "contract": "MidasPauseManager", + "src": "contracts/access/MidasPauseManager.sol:46" + }, + { + "label": "globalPaused", + "offset": 8, + "slot": "53", + "type": "t_bool", + "contract": "MidasPauseManager", + "src": "contracts/access/MidasPauseManager.sol:51" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes4": { + "label": "bytes4", + "numberOfBytes": "4" + }, + "t_contract(IMidasAccessControl)22015": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_bytes4,t_bool))": { + "label": "mapping(address => mapping(bytes4 => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes4,t_bool)": { + "label": "mapping(bytes4 => bool)", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "1433e2d788a6ee0c4a05fb51b733f4d6036a37ba376ab55e5cd1fe9a8390445c": { + "address": "0xDeAB4a23e6dC1CfF64ad6CD769E2683F0a37C584", + "txHash": "0x3560571b16fea02e1a8854e2d124640aae59647173311b69245fe2b23ddba8eb", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_balances", + "offset": 0, + "slot": "51", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:40" + }, + { + "label": "_allowances", + "offset": 0, + "slot": "52", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:42" + }, + { + "label": "_totalSupply", + "offset": 0, + "slot": "53", + "type": "t_uint256", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:44" + }, + { + "label": "_name", + "offset": 0, + "slot": "54", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:46" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "55", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:47" + }, + { + "label": "__gap", + "offset": 0, + "slot": "56", + "type": "t_array(t_uint256)45_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:376" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC20PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol:48" + }, + { + "label": "accessControl", + "offset": 0, + "slot": "201", + "type": "t_contract(IMidasAccessControl)37060", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:34" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:17" + }, + { + "label": "metadata", + "offset": 0, + "slot": "302", + "type": "t_mapping(t_bytes32,t_bytes_storage)", + "contract": "mToken", + "src": "contracts/mToken.sol:49" + }, + { + "label": "_mintRateLimits", + "offset": 0, + "slot": "303", + "type": "t_struct(WindowRateLimits)39129_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:54" + }, + { + "label": "clawbackReceiver", + "offset": 0, + "slot": "306", + "type": "t_address", + "contract": "mToken", + "src": "contracts/mToken.sol:59" + }, + { + "label": "_inClawback", + "offset": 20, + "slot": "306", + "type": "t_bool", + "contract": "mToken", + "src": "contracts/mToken.sol:64" + }, + { + "label": "_name", + "offset": 0, + "slot": "307", + "type": "t_string_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:69" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "308", + "type": "t_string_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:74" + }, + { + "label": "__gap", + "offset": 0, + "slot": "309", + "type": "t_array(t_uint256)44_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:79" + }, + { + "label": "___gap", + "offset": 0, + "slot": "353", + "type": "t_array(t_uint256)50_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:84" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)44_storage": { + "label": "uint256[44]", + "numberOfBytes": "1408" + }, + "t_array(t_uint256)45_storage": { + "label": "uint256[45]", + "numberOfBytes": "1440" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_contract(IMidasAccessControl)37060": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bytes_storage)": { + "label": "mapping(bytes32 => bytes)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)39119_storage)": { + "label": "mapping(uint256 => struct RateLimitLibrary.WindowRateLimitConfig)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Set)14575_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(UintSet)15047_storage": { + "label": "struct EnumerableSetUpgradeable.UintSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)14575_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(WindowRateLimitConfig)39119_storage": { + "label": "struct RateLimitLibrary.WindowRateLimitConfig", + "members": [ + { + "label": "limit", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amountInFlight", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "window", + "type": "t_uint256", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(WindowRateLimits)39129_storage": { + "label": "struct RateLimitLibrary.WindowRateLimits", + "members": [ + { + "label": "windows", + "type": "t_struct(UintSet)15047_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "configs", + "type": "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)39119_storage)", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "e72a3c989e2ac042e9cfc80fe6daffc264768e4ed7d0a8be7ee31b8bebcfe942": { + "address": "0x69237996971F483B2228aF23738B78EBEf4e7Da4", + "txHash": "0x72c5219b909742c9e066bce78681e4ff919a95e85c8379f4d0b8e789e894fb7e", + "layout": { + "solcVersion": "0.8.34", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_balances", + "offset": 0, + "slot": "51", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:40" + }, + { + "label": "_allowances", + "offset": 0, + "slot": "52", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:42" + }, + { + "label": "_totalSupply", + "offset": 0, + "slot": "53", + "type": "t_uint256", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:44" + }, + { + "label": "_name", + "offset": 0, + "slot": "54", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:46" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "55", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:47" + }, + { + "label": "__gap", + "offset": 0, + "slot": "56", + "type": "t_array(t_uint256)45_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:376" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC20PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol:48" + }, + { + "label": "accessControl", + "offset": 0, + "slot": "201", + "type": "t_contract(IMidasAccessControl)37060", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29", + "retypedFrom": "MidasAccessControl" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:34" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:17" + }, + { + "label": "metadata", + "offset": 0, + "slot": "302", + "type": "t_mapping(t_bytes32,t_bytes_storage)", + "contract": "mToken", + "src": "contracts/mToken.sol:49" + }, + { + "label": "_mintRateLimits", + "offset": 0, + "slot": "303", + "type": "t_struct(WindowRateLimits)39129_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:54" + }, + { + "label": "clawbackReceiver", + "offset": 0, + "slot": "306", + "type": "t_address", + "contract": "mToken", + "src": "contracts/mToken.sol:59" + }, + { + "label": "_inClawback", + "offset": 20, + "slot": "306", + "type": "t_bool", + "contract": "mToken", + "src": "contracts/mToken.sol:64" + }, + { + "label": "_name", + "offset": 0, + "slot": "307", + "type": "t_string_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:69" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "308", + "type": "t_string_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:74" + }, + { + "label": "__gap", + "offset": 0, + "slot": "309", + "type": "t_array(t_uint256)44_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:79" + }, + { + "label": "___gap", + "offset": 0, + "slot": "353", + "type": "t_array(t_uint256)50_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:84" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)44_storage": { + "label": "uint256[44]", + "numberOfBytes": "1408" + }, + "t_array(t_uint256)45_storage": { + "label": "uint256[45]", + "numberOfBytes": "1440" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_contract(IMidasAccessControl)37060": { + "label": "contract IMidasAccessControl", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bytes_storage)": { + "label": "mapping(bytes32 => bytes)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)39119_storage)": { + "label": "mapping(uint256 => struct RateLimitLibrary.WindowRateLimitConfig)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Set)14575_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(UintSet)15047_storage": { + "label": "struct EnumerableSetUpgradeable.UintSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)14575_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(WindowRateLimitConfig)39119_storage": { + "label": "struct RateLimitLibrary.WindowRateLimitConfig", + "members": [ + { + "label": "limit", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amountInFlight", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "window", + "type": "t_uint256", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(WindowRateLimits)39129_storage": { + "label": "struct RateLimitLibrary.WindowRateLimits", + "members": [ + { + "label": "windows", + "type": "t_struct(UintSet)15047_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "configs", + "type": "t_mapping(t_uint256,t_struct(WindowRateLimitConfig)39119_storage)", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, "c518b0b692d61f7c9db1bc9da6cc655d25c957482487018d343f2cb778fe71f1": { "address": "0xdEaA809C72b8Aabbe9037F497B2779Ad3f7d4f87", "txHash": "0xd4470fc2dba4177ae6937ab4fa32b612f94b3b5e6936d2f26d7ce2ac5621b326", diff --git a/ERRORS.md b/ERRORS.md new file mode 100644 index 00000000..5935ea01 --- /dev/null +++ b/ERRORS.md @@ -0,0 +1,107 @@ +# Custom Errors + +Auto-generated by `scripts/generate-errors-readme.ts` from compiled artifacts. +Total: 100 errors. + +| Selector | Error | Declared in | +| --- | --- | --- | +| `0xb90f05dc` | `AddressHasEntityId()` | IAllowListV2 | +| `0x2eb252a6` | `AddressHasProtocolPermissions()` | IAllowListV2 | +| `0xa9248256` | `AllowanceExceeded(uint256,uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0xa741a045` | `AlreadySet()` | IAllowListV2 | +| `0x7c9a1cf9` | `AlreadyVoted()` | IMidasTimelockManager, MidasTimelockManager | +| `0x5bb53877` | `AmountLessThanMin(uint256,uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0xe2ce9413` | `AmountSDOverflowed(uint256)` | MidasLzMintBurnOFTAdapter, MidasLzOFT, MidasLzOFTAdapter | +| `0x4e83a9b9` | `AssetMismatch(address,address)` | DepositVaultWithMorpho, RedemptionVaultWithMorpho | +| `0xf3d3036a` | `AutoInvestFailed(bytes)` | DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho | +| `0xa554dcdf` | `BadData()` | IAllowListV2 | +| `0x31ff61e9` | `Blacklisted(bytes32,address)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, MidasAuthLibrary, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB, mToken, mTokenPermissioned | +| `0x129ea30c` | `CannotRevokeFromSelf(bytes32,address)` | IMidasAccessControl, MidasAccessControl | +| `0x5e393b26` | `CodeSizeZero()` | IAllowListV2 | +| `0xef5315f6` | `DelayIsAlreadySet()` | IMidasAccessControl, MidasAccessControl | +| `0xc73b9d7c` | `Deprecated()` | IAllowListV2 | +| `0x521299a9` | `EmptyArray()` | IMidasAccessControl, MidasAccessControl | +| `0x5534f9b3` | `FeeExceedsAmount(uint256,uint256)` | IRedemptionVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0xee90c468` | `Forbidden()` | IMidasAccessControl, MidasAccessControl | +| `0x2e48edb8` | `InstantFeeOutOfBounds(uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0x5438233f` | `InstantShareTooHigh(uint256,uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0x7cb769dc` | `InsufficientMsgValue(uint256,uint256)` | IMidasLzVaultComposerSync, MidasLzVaultComposerSync | +| `0x9ea78c15` | `InsufficientWithdrawnAmount(uint256,uint256)` | RedemptionVaultWithAave | +| `0x8e4c8aa6` | `InvalidAddress(address)` | Blacklistable, CompositeDataFeed, CompositeDataFeedMultiply, CustomAggregatorV3CompatibleFeed, CustomAggregatorV3CompatibleFeedGrowth, DataFeed, DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, Greenlistable, ManageableVault, MidasAccessControl, MidasAccessControlTimelockController, MidasAxelarVaultExecutable, MidasInitializable, MidasLzVaultComposerSync, MidasPauseManager, MidasTimelockManager, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB, WithMidasAccessControl, WithSanctionsList, mToken, mTokenPermissioned | +| `0x2c5211c6` | `InvalidAmount()` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0x4fbe5dba` | `InvalidDelay()` | CompositeDataFeed, CompositeDataFeedMultiply, CustomAggregatorV3CompatibleFeed, CustomAggregatorV3CompatibleFeedGrowth, DataFeed, DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, Greenlistable, ManageableVault, MidasAccessControl, MidasAuthLibrary, MidasPauseManager, MidasTimelockManager, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB, WithSanctionsList, mToken, mTokenPermissioned | +| `0xb5863604` | `InvalidDelegate()` | MidasLzMintBurnOFTAdapter, MidasLzOFT, MidasLzOFTAdapter | +| `0x0fbdec0a` | `InvalidEndpointCall()` | MidasLzMintBurnOFTAdapter, MidasLzOFT, MidasLzOFTAdapter | +| `0x2f38c6ee` | `InvalidFee(uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0x1e9714b0` | `InvalidLocalDecimals()` | MidasLzMintBurnOFTAdapter, MidasLzOFT, MidasLzOFTAdapter | +| `0x531febf4` | `InvalidMaxPendingOperationsPerProposer()` | IMidasTimelockManager, MidasTimelockManager | +| `0xd706201a` | `InvalidMinMaxInstantFee(uint256,uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0x2ea32193` | `InvalidNewLimit(uint256,uint256)` | IMToken, mToken, mTokenPermissioned | +| `0xb19e5c8b` | `InvalidNewMTokenRate()` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0x9a6d49cd` | `InvalidOptions(bytes)` | MidasLzMintBurnOFTAdapter, MidasLzOFT, MidasLzOFTAdapter | +| `0x0d240b73` | `InvalidPreflightError(bytes)` | IMidasTimelockManager, MidasTimelockManager | +| `0x347e7b4a` | `InvalidRequestSequence(uint256,uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0x09c8cebf` | `InvalidRounding(uint256,uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0x8f82da5a` | `InvalidSecurityCouncilMembersLength()` | IMidasTimelockManager, MidasTimelockManager | +| `0x147b28ba` | `InvalidTokenRate(address)` | MidasLzVaultComposerSync | +| `0xa75b64ce` | `InvalidTokenRate(uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0xc3bcd0b5` | `LessThanMinAmountFirstDeposit(uint256,uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault | +| `0x5373352a` | `LzTokenUnavailable()` | MidasLzMintBurnOFTAdapter, MidasLzOFT, MidasLzOFTAdapter | +| `0x02af5858` | `MaxAmountPerRequestExceeded(uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault | +| `0xa1cca21b` | `MismatchArrays(uint256,uint256)` | IMidasAccessControl, MidasAccessControl | +| `0x47298962` | `NoFunctionPermission(bytes32,bytes4,address)` | CompositeDataFeed, CompositeDataFeedMultiply, CustomAggregatorV3CompatibleFeed, CustomAggregatorV3CompatibleFeedGrowth, DataFeed, DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, Greenlistable, ManageableVault, MidasAccessControl, MidasAuthLibrary, MidasPauseManager, MidasTimelockManager, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB, WithSanctionsList, mToken, mTokenPermissioned | +| `0x7578d2bd` | `NoMsgValueExpected()` | IMidasLzVaultComposerSync, MidasLzVaultComposerSync | +| `0xcfe2af83` | `NonZeroEntityIdMustBeChangedToZero()` | IAllowListV2 | +| `0xf6ff4fb7` | `NoPeer(uint32)` | MidasLzMintBurnOFTAdapter, MidasLzOFT, MidasLzOFTAdapter | +| `0x9f704120` | `NotEnoughNative(uint256)` | MidasLzMintBurnOFTAdapter, MidasLzOFT, MidasLzOFTAdapter | +| `0xd3659815` | `NotGreenlisted(address,bytes32)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, MidasAuthLibrary, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB, mTokenPermissioned | +| `0x341da969` | `NoTimelockDelayForRole()` | IMidasTimelockManager, MidasTimelockManager | +| `0x48cfb14d` | `NotInSecurityCouncil()` | IMidasTimelockManager, MidasTimelockManager | +| `0xec7cdc0a` | `NotSelfCall()` | IRedemptionVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0x0d6c7be9` | `NotService(address)` | MidasAxelarVaultExecutable | +| `0x91ac5e4f` | `OnlyEndpoint(address)` | IMidasLzVaultComposerSync, MidasLzMintBurnOFTAdapter, MidasLzOFT, MidasLzOFTAdapter, MidasLzVaultComposerSync | +| `0xc26bebcc` | `OnlyPeer(uint32,bytes32)` | MidasLzMintBurnOFTAdapter, MidasLzOFT, MidasLzOFTAdapter | +| `0x14d4a4e8` | `OnlySelf()` | MidasLzMintBurnOFTAdapter, MidasLzOFT, MidasLzOFTAdapter | +| `0xa19dbf00` | `OnlySelf(address)` | IMidasAxelarVaultExecutable, IMidasLzVaultComposerSync, MidasAxelarVaultExecutable, MidasLzVaultComposerSync | +| `0x84fb3f0d` | `OnlyValidComposeCaller(address)` | IMidasLzVaultComposerSync, MidasLzVaultComposerSync | +| `0x0f428110` | `OnlyValidExecutableTokenId(bytes32)` | IMidasAxelarVaultExecutable, MidasAxelarVaultExecutable | +| `0xfad6ff9f` | `OperationAlreadyPending()` | IMidasTimelockManager, MidasTimelockManager | +| `0xe436bddf` | `OperationNotPending()` | IMidasTimelockManager, MidasTimelockManager | +| `0x31224282` | `Paused(address,bytes4)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, ManageableVault, PauseGuardsLibrary, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB, mToken, mTokenPermissioned | +| `0xe72b517a` | `PaymentTokenAlreadyAdded(address)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0x9b53c3d1` | `PaymentTokenNotExists(address)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0xcc9fca97` | `PendingSetCouncilOperationExists()` | IMidasTimelockManager, MidasTimelockManager | +| `0x7e18bb60` | `PoolNotSet(address)` | DepositVaultWithAave, RedemptionVaultWithAave | +| `0x0c3c0dd1` | `PreflightCallUnexpectedSuccess()` | IMidasTimelockManager, MidasTimelockManager | +| `0x2918eb1d` | `PriceVariationExceeded(uint256,uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0xa74c1c5f` | `RateLimitExceeded()` | MidasLzMintBurnOFTAdapter | +| `0xffc0fd93` | `RenounceOwnershipDisabled()` | IAllowListV2 | +| `0xe8ada359` | `RequestIdTooHigh(uint256,uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0xc568fc41` | `RequestNotExists(uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0x3aa00981` | `RoleAdminMismatch(bytes32,bytes32)` | IMidasAccessControl, MidasAccessControl | +| `0xc7529dd2` | `RolePreflightSucceeded(bytes32,uint32,bool,bool)` | CompositeDataFeed, CompositeDataFeedMultiply, CustomAggregatorV3CompatibleFeed, CustomAggregatorV3CompatibleFeedGrowth, DataFeed, DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, Greenlistable, IMidasTimelockManager, ManageableVault, MidasAccessControl, MidasPauseManager, MidasTimelockManager, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB, WithSanctionsList, mToken, mTokenPermissioned | +| `0x95b6beba` | `SameAddressValue(address)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0x738bd136` | `SameBoolValue(bool)` | Blacklistable, CompositeDataFeed, CompositeDataFeedMultiply, CustomAggregatorV3CompatibleFeed, CustomAggregatorV3CompatibleFeedGrowth, DataFeed, DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, Greenlistable, ManageableVault, MidasPauseManager, MidasTimelockManager, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB, WithMidasAccessControl, WithSanctionsList, mToken, mTokenPermissioned | +| `0x63d72e07` | `Sanctioned(address)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB, WithSanctionsList | +| `0x1ad26fd3` | `SenderIsNotTimelock(bytes32,bytes4,address)` | CompositeDataFeed, CompositeDataFeedMultiply, CustomAggregatorV3CompatibleFeed, CustomAggregatorV3CompatibleFeedGrowth, DataFeed, DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, Greenlistable, ManageableVault, MidasAccessControl, MidasAuthLibrary, MidasPauseManager, MidasTimelockManager, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB, WithSanctionsList, mToken, mTokenPermissioned | +| `0xfab1aa4d` | `SenderNotThis(address)` | MidasLzMintBurnOFTAdapter | +| `0x8351eea7` | `SimulationResult(bytes)` | MidasLzMintBurnOFTAdapter, MidasLzOFT, MidasLzOFTAdapter | +| `0x71c4efed` | `SlippageExceeded(uint256,uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, MidasLzMintBurnOFTAdapter, MidasLzOFT, MidasLzOFTAdapter, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0xf58f733a` | `SupplyCapExceeded()` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault | +| `0x76148ac4` | `TimelockAlreadySet()` | IMidasTimelockManager, MidasTimelockManager | +| `0x774bf8ce` | `TimelockOperationNotReady()` | IMidasTimelockManager, MidasTimelockManager | +| `0xa0e8eaef` | `TokenAddressMismatch(address,address,address)` | MidasAxelarVaultExecutable, MidasLzVaultComposerSync | +| `0x6dfabd3f` | `TokenNotInPool(address,address)` | DepositVaultWithAave, RedemptionVaultWithAave | +| `0x1778db9d` | `TooManyPendingOperations()` | IMidasTimelockManager, MidasTimelockManager | +| `0x07d8b99c` | `UnexpectedOperationStatus(uint8)` | IMidasTimelockManager, MidasTimelockManager | +| `0xae46e8c4` | `UnexpectedRequestStatus(uint256,uint8)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0x24fc126b` | `UnknownPaymentToken(address)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, IDepositVault, IManageableVault, IRedemptionVault, ManageableVault, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB | +| `0x9629ca18` | `UnknownWindowLimit(uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, ManageableVault, RateLimitLibrary, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB, mToken, mTokenPermissioned | +| `0xa4ea87cc` | `UnsupportedUSTBToken(address)` | DepositVaultWithUSTB | +| `0xafa3b1d4` | `UserFacingRoleNotAllowed(bytes32)` | CompositeDataFeed, CompositeDataFeedMultiply, CustomAggregatorV3CompatibleFeed, CustomAggregatorV3CompatibleFeedGrowth, DataFeed, DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, Greenlistable, ManageableVault, MidasAccessControl, MidasAuthLibrary, MidasPauseManager, MidasTimelockManager, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB, WithSanctionsList, mToken, mTokenPermissioned | +| `0xfef1b6c4` | `USTBFeeNotZero(uint256)` | DepositVaultWithUSTB | +| `0x34455819` | `VaultNotSet(address)` | DepositVaultWithMorpho, RedemptionVaultWithMorpho | +| `0xa8827d28` | `WindowLimitExceeded(uint256,uint256,uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, ManageableVault, RateLimitLibrary, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB, mToken, mTokenPermissioned | +| `0xfe84fc4a` | `WindowTooShort(uint256)` | DepositVault, DepositVaultWithAave, DepositVaultWithMToken, DepositVaultWithMorpho, DepositVaultWithUSTB, ManageableVault, RateLimitLibrary, RedemptionVault, RedemptionVaultWithAave, RedemptionVaultWithMToken, RedemptionVaultWithMorpho, RedemptionVaultWithUSTB, mToken, mTokenPermissioned | +| `0x825c287a` | `ZeroMTokenReceived(uint256)` | DepositVaultWithMToken | +| `0xdc6d336e` | `ZeroShares(uint256)` | DepositVaultWithMorpho | diff --git a/LICENSE b/LICENSE index e72bfdda..389c6837 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,80 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 +Business Source License 1.1 - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. +License text copyright © 2017 MariaDB Corporation Ab, All Rights Reserved. +"Business Source License" is a trademark of MariaDB Corporation Ab. - Preamble +Parameters - The GNU General Public License is a free, copyleft license for -software and other kinds of works. +Licensor: Midas +Licensed Work: Midas EVM Contracts +Change Date: 2036-01-01 +Change License: GPL-2.0 or any later version - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. +Additional Use Grant: None - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. +Terms - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. - The precise terms and conditions for copying, distribution and -modification follow. +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). - TERMS AND CONDITIONS +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. - 0. Definitions. +MariaDB hereby grants you permission to use this License’s text to license +your works, and to refer to it using the trademark “Business Source License”, +as long as you comply with the Covenants of Licensor below. - "This License" refers to version 3 of the GNU General Public License. +Covenants of Licensor - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. +In consideration of the right to use this License’s text and the “Business +Source License” name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where “compatible” means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text “None”. - A "covered work" means either the unmodified Program or a work based -on the Program. +3. To specify a Change Date. - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. \ No newline at end of file +4. Not to modify this License in any other way. diff --git a/README.md b/README.md index 1f7b288a..3758c59f 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,17 @@ This repository contains EVM smart contracts for the Midas protocol. - [Repository Structure](#repository-structure) - [Building and Testing](#building-and-testing) - [Architecture Overview](#architecture-overview) -- [Core Contracts](#core-contracts) -- [Crosschain Contracts](#crosschain-contracts-axelar-layerzero) -- [Deployed Contract Addresses](#deployed-contract-addresses) +- [Contracts Overview](#contracts-overview) + - [Access Control Contracts](#access-control-contracts) + - [mTokens](#mtokens) + - [Data Feeds and Aggregators](#data-feeds-and-aggregators) + - [Vaults](#vaults) + - [Miscellaneous Contracts](#miscellaneous-contracts) - [Deployment](#deployment) - [Upgradeability](#upgradability) - [Documentation](#documentation) - [Development](#development) +- [Deployed Contract Addresses](#deployed-contract-addresses) ## Prerequisites @@ -40,15 +44,14 @@ In the root of the repo, create a `.env` file from [.env.example](./.env.example midas-contracts/ ├── contracts/ # Smart contract source code │ ├── abstract/ # Abstract base contracts -│ ├── access/ # Access control contracts -│ ├── feeds/ # Price feed contracts +│ ├── access/ # Access control, timelock and pause contracts +│ ├── feeds/ # Price feed and aggregator contracts │ ├── interfaces/ # Contract interfaces │ ├── libraries/ # Contract libraries -│ ├── misc/ # Miscellaneous contracts (LayerZero, Axelar, Custom price feed adapters) -│ ├── products/ # Individual mToken implementations +│ ├── misc/ # Miscellaneous contracts (LayerZero, Axelar, data feed adapters) │ ├── testers/ # Non-production contracts that are used in tests │ ├── mocks/ # Mocked contracts used in tests and some testnet deployments -│ ├── *.sol # Core smart contracts +│ ├── *.sol # Core smart contracts (mTokens and vaults) │ └── ... ├── config/ # Configuration files │ ├── constants/ # Addresses and constants @@ -110,13 +113,22 @@ yarn slither:summary ## Architecture Overview +The protocol is built around a few simple ideas: + +- **mTokens** are the ERC20 tokens issued by the protocol (e.g. mTBILL, mBASIS, mBTC). +- **Vaults** are the entry points for users. They handle minting (deposits) and redemption (burns), calculate fees and exchange rates, and move funds. +- **Data Feeds and Aggregators** provide the prices that vaults use to compute exchange rates. +- **Access control contracts** sit underneath everything and control who is allowed to do what: roles, function-level permissions, timelock and pause management. +- **Miscellaneous contracts** connect the protocol to external systems: cross-chain bridges and third-party price feed adapters. + ### Core Components 1. **mTokens**: ERC20 tokens (e.g., mTBILL, mBASIS, mBTC) 2. **Vaults**: Smart contracts managing minting (deposits) and redemption processes 3. **Access Control**: Role-based permission system for managing protocol operations -4. **Price Feeds**: Oracle integrations for conversion rates -5. **Cross-Chain**: LayerZero and Axelar integrations for multi-chain operations +4. **Timelock and Pause**: Delayed execution of admin actions and a global pause mechanism +5. **Price Feeds**: Oracle integrations for conversion rates +6. **Cross-Chain**: LayerZero and Axelar integrations for multi-chain operations ### System Flow @@ -136,21 +148,30 @@ yarn slither:summary 4. For instant redemptions (sync): Payment tokens are transferred to user immediately 5. For request redemptions (async): Admin fulfills the request after off-chain processing -## Core Contracts +## Contracts Overview -### **MidasAccessControl** ([`contracts/access/MidasAccessControl.sol`](contracts/access/MidasAccessControl.sol)) +### Access Control Contracts + +These contracts form the base layer of the protocol. They do not move user funds themselves, but they decide who is allowed to call protected functions, add a delay to sensitive admin actions, and allow the protocol to be paused. + +#### **MidasAccessControl** ([`contracts/access/MidasAccessControl.sol`](contracts/access/MidasAccessControl.sol)) Centralized access control contract managing all roles across the protocol **Key Features:** - Role-based access control +- Function-level access control - on top of plain roles, access can be scoped to a specific function on a specific target contract. A function permission is identified by `(masterRole, targetContract, functionSelector)`, and only the accounts enabled for that permission can call the function. Grant operators manage who is enabled for each permission. +- Per-role timelock delays - each role (and each function permission) can have its own timelock delay, so changes to it only take effect after a configured period - Greenlist/blacklist management **Key Functions:** - `grantRole()` - grants role to a specific address. Can be invoked only by role admin - `revokeRole()` - revokes role from a specific address. Can be invoked only by role admin +- `setPermissionRoleMult()` - enables or disables accounts for a specific function-level permission `(masterRole, targetContract, functionSelector)` +- `setGrantOperatorRoleMult()` - sets the operators that are allowed to manage a given function permission +- `setRoleDelayMult()` / `setDefaultDelay()` - configure the timelock delay for individual roles or the default delay The default role admin for all roles is `defaultAdmin`. Only exceptions are: @@ -160,6 +181,7 @@ The default role admin for all roles is `defaultAdmin`. Only exceptions are: All roles in the system are documented in [`ROLES.md`](./ROLES.md). This file is auto-generated and contains: - **Common Roles**: Roles shared across all contracts: + - `defaultAdmin`: `0x0000000000000000000000000000000000000000000000000000000000000000` - `greenlisted`: Allows access to vault operations (if greenlist is enforced) - `blacklisted`: Prevents token transfers and vaults access @@ -172,9 +194,52 @@ All roles in the system are documented in [`ROLES.md`](./ROLES.md). This file is - `depositVaultAdmin` - Can manage deposit vault operations - `redemptionVaultAdmin` - Can manage redemption vault operations -### **mToken** ([`contracts/mToken.sol`](contracts/mToken.sol)) +#### **MidasTimelockManager** ([`contracts/access/MidasTimelockManager.sol`](contracts/access/MidasTimelockManager.sol)) -Abstract base contract for all mToken implementations +Manages timelock scheduling, security council votes and operation details. Sensitive admin actions (for example access control changes and upgrades) are not executed directly. Instead they are scheduled here, optionally reviewed by a security council, and only executed after a delay. + +**Key Features:** + +- Scheduling of timelock operations, single and in bulk +- Security council with versioning, used to vote for or veto operations +- Expiration and dispute periods for pending operations +- A hard cap on the number of pending operations per proposer + +**Key Functions:** + +- `scheduleTimelockOperation()` - schedules a single operation to be executed after the delay +- `bulkScheduleTimelockOperation()` - schedules multiple operations at once +- `executeTimelockOperation()` - executes a scheduled operation once it is ready +- `voteForExecution()` / `voteForVeto()` - security council voting to approve or block an operation +- `setSecurityCouncil()` - updates the security council members + +#### **Timelock Controllers** + +The actual delayed execution is performed by OpenZeppelin based timelock controllers: + +- **MidasTimelockController** ([`contracts/access/MidasTimelockController.sol`](contracts/access/MidasTimelockController.sol)) - A standard OpenZeppelin `TimelockController` extended with getters for proposers and executors. +- **MidasAccessControlTimelockController** ([`contracts/access/MidasAccessControlTimelockController.sol`](contracts/access/MidasAccessControlTimelockController.sol)) - A `TimelockController` that is driven by `MidasTimelockManager`. It is used to apply access control changes through the timelock flow. + +#### **MidasPauseManager** ([`contracts/access/MidasPauseManager.sol`](contracts/access/MidasPauseManager.sol)) + +Global manager for pausing and unpausing protocol functions. Pausing can be applied globally, per contract, or per individual function, which gives fine-grained control during incidents. + +**Key Features:** + +- Global pause, per-contract pause and per-function pause +- Separate roles for pausing and unpausing +- Configurable delays for pause and unpause + +**Key Functions:** + +- `pause()` / `unpause()` - change the pause status for a contract or function +- Per-function granularity, so only the affected operations can be stopped + +### mTokens + +#### **mToken** ([`contracts/mToken.sol`](contracts/mToken.sol)) + +Base contract for all mToken implementations **Key Features:** @@ -188,9 +253,45 @@ Abstract base contract for all mToken implementations - `burn(address from, uint256 amount)` - Burn tokens from any address (requires burner role) - `pause()` / `unpause()` - Pause/unpause transfers (requires pauser role) -### **Vaults** +**mToken Variations:** + +- **mTokenPermissioned** ([`contracts/mTokenPermissioned.sol`](contracts/mTokenPermissioned.sol)) - An mToken with fully permissioned transfers, where transfers are only allowed between approved addresses. + +### Data Feeds and Aggregators + +Vaults need prices to compute exchange rates. The protocol separates this into two layers: **aggregators** that publish or expose a price using the Chainlink `AggregatorV3` interface, and **data feeds** that wrap an aggregator, validate the price and convert it to a common 18 decimals format. + +#### **DataFeed** ([`contracts/feeds/DataFeed.sol`](contracts/feeds/DataFeed.sol)) + +Wraps a Chainlink `AggregatorV3` price feed, validates the price (max/min/staleness) and converts answers to 18 decimals format + +**Key Functions:** + +- `getDataInBase18()` - View function, returns the validated and converted price with 18 decimals. Checks price for min/max allowed values, checks that it is not stale + +**DataFeed Variations:** + +- **CompositeDataFeed** ([`contracts/feeds/CompositeDataFeed.sol`](contracts/feeds/CompositeDataFeed.sol)) - Computes the ratio of two underlying data feeds (numerator ÷ denominator). +- **CompositeDataFeedMultiply** ([`contracts/feeds/CompositeDataFeedMultiply.sol`](contracts/feeds/CompositeDataFeedMultiply.sol)) - Computes the product of two underlying data feeds (numerator × denominator). + +#### **CustomAggregatorV3CompatibleFeed** ([`contracts/feeds/CustomAggregatorV3CompatibleFeed.sol`](contracts/feeds/CustomAggregatorV3CompatibleFeed.sol)) + +Custom price aggregator compatible with Chainlink's `AggregatorV3` interface. Used to publish mToken prices on-chain + +**Key Functions:** + +- `setRoundData()` - function to push the price on-chain +- `setRoundDataSafe()` - same as `setRoundData()` but also performs a deviation check by comparing current and new prices +- `latestRoundData()` - View function, returns latest submitted price with submission details (check [AggregatorV3Interface.sol](@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol)) + +**CustomAggregatorV3CompatibleFeed Variations:** -There are 2 types of vaults - Deposit vaults and Redemption vaults. Also each type of vault have different variations (like USTB, Swapper, BUIDL etc.) +- **CustomAggregatorV3CompatibleFeedGrowth** ([`contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol`](contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol)) - Includes a growth parameter that automatically increases the feed price over time. +- **CustomAggregatorV3CompatibleFeedAdjusted** ([`contracts/feeds/CustomAggregatorV3CompatibleFeedAdjusted.sol`](contracts/feeds/CustomAggregatorV3CompatibleFeedAdjusted.sol)) - A proxy feed that adjusts the price of an underlying Chainlink compatible feed by a signed percentage. A positive percentage raises the reported price, a negative percentage lowers it. + +### Vaults + +There are 2 types of vaults - Deposit vaults and Redemption vaults. Also each type of vault have different variations (like USTB, Swapper etc.) **Common Key Features:** @@ -219,7 +320,7 @@ Both vaults support two execution modes: - Admin approves or rejects after off-chain processing - Used when instant liquidity is insufficient or instant operations are disabled. Also for fiat operations -### **DepositVault** ([`contracts/DepositVault.sol`](contracts/DepositVault.sol)) +#### **DepositVault** ([`contracts/DepositVault.sol`](contracts/DepositVault.sol)) Manages the minting process for mTokens. Users deposit payment tokens to receive mTokens. @@ -239,9 +340,12 @@ Manages the minting process for mTokens. Users deposit payment tokens to receive **Vault Variations:** -- USTB ([`contracts/DepositVaultWithUSTB.sol`](contracts/DepositVaultWithUSTB.sol)) - Automatically invests USDC into USTB tokens before transferring proceeds to the recipient. +- **USTB** ([`contracts/DepositVaultWithUSTB.sol`](contracts/DepositVaultWithUSTB.sol)) - Automatically invests USDC into USTB tokens before transferring proceeds to the recipient. +- **Aave** ([`contracts/DepositVaultWithAave.sol`](contracts/DepositVaultWithAave.sol)) - Invests deposited proceeds into an Aave V3 Pool. +- **Morpho** ([`contracts/DepositVaultWithMorpho.sol`](contracts/DepositVaultWithMorpho.sol)) - Invests deposited proceeds into a Morpho Vault. +- **mToken** ([`contracts/DepositVaultWithMToken.sol`](contracts/DepositVaultWithMToken.sol)) - Invests deposited proceeds into another mToken. -### **RedemptionVault** ([`contracts/RedemptionVault.sol`](contracts/RedemptionVault.sol)) +#### **RedemptionVault** ([`contracts/RedemptionVault.sol`](contracts/RedemptionVault.sol)) Manages the redemption process for mTokens. Burns mTokens from a user and transfers payment tokens in exchange @@ -262,74 +366,46 @@ Manages the redemption process for mTokens. Burns mTokens from a user and transf **Vault Variations:** -- Swapper ([`contracts/RedemptionVaultWithSwapper.sol`](contracts/RedemptionVaultWithSwapper.sol)) - Uses an external liquidity source to exchange one mToken for another and redeems the obtained mTokens through a different Midas redemption vault. This flow is activated only when there is insufficient liquidity in the current Redemption Vault. -- BUIDL ([`contracts/RedemptionVaultWithBUIDL.sol`](contracts/RedemptionVaultWithBUIDL.sol)) (*deprecated*) - Stores pending liquidity as BUIDL tokens. When the vault has insufficient USDC liquidity to fulfill an instant redemption, BUIDL tokens are redeemed for USDC and used to complete the redemption. -- USTB ([`contracts/RedemptionVaultWithUSTB.sol`](contracts/RedemptionVaultWithUSTB.sol)) - Stores pending liquidity as USTB tokens. When the vault has insufficient USDC liquidity to fulfill an instant redemption, USTB tokens are redeemed for USDC and used to complete the redemption. - -### **DataFeed** ([`contracts/feeds/DataFeed.sol`](contracts/feeds/DataFeed.sol)) +- **USTB** ([`contracts/RedemptionVaultWithUSTB.sol`](contracts/RedemptionVaultWithUSTB.sol)) - Stores pending liquidity as USTB tokens. When the vault has insufficient USDC liquidity to fulfill an instant redemption, USTB tokens are redeemed for USDC and used to complete the redemption. +- **Aave** ([`contracts/RedemptionVaultWithAave.sol`](contracts/RedemptionVaultWithAave.sol)) - Sources liquidity by withdrawing from an Aave V3 Pool. +- **Morpho** ([`contracts/RedemptionVaultWithMorpho.sol`](contracts/RedemptionVaultWithMorpho.sol)) - Sources liquidity by withdrawing from a Morpho Vault. +- **mToken** ([`contracts/RedemptionVaultWithMToken.sol`](contracts/RedemptionVaultWithMToken.sol)) - Uses an external liquidity source to exchange one mToken for another and redeems the obtained mTokens through a different Midas redemption vault. This flow is activated only when there is insufficient liquidity in the current Redemption Vault. (This contract replaces the former `RedemptionVaultWithSwapper`; its storage layout is preserved for safe upgrades.) -Wraps Chainlink AggregatorV3 price feeds, validates the price (max/min/staleness) and converts answers to 18 decimals format +### Miscellaneous Contracts -**Key Functions:** - -- `getDataInBase18()`- View function, returns the validated and converted price with 18 decimals. Checks price for min/max allowed values, checks that its not stale +These contracts are supporting pieces. Most of them do not hold protocol state or user funds. They connect Midas to external price oracles and to other chains. -**DataFeed Variations:** +#### Data Feed Adapters (`contracts/misc/adapters/`) -- CompositeDataFeed ([`contracts/feeds/CompositeDataFeed.sol`](contracts/feeds/CompositeDataFeed.sol)) - computing the ratio of two underlying data feeds (numerator ÷ denominator) +Adapters expose third-party price sources through the Chainlink `AggregatorV3` interface, so they can be consumed by the protocol `DataFeed` contracts in a uniform way. They share a common base ([`ChainlinkAdapterBase.sol`](contracts/misc/adapters/ChainlinkAdapterBase.sol)). Available adapters include: -### **CustomAggregatorV3CompatibleFeed** ([`contracts/feeds/CustomAggregatorV3CompatibleFeed.sol`](contracts/feeds/CustomAggregatorV3CompatibleFeed.sol)) +- Generic protocol ports: `PythChainlinkAdapter`, `StorkChainlinkAdapter`, `BandStdChailinkAdapter` +- ERC4626 vaults: `ERC4626ChainlinkAdapter` +- Liquid staking and specific assets: `WstEthChainlinkAdapter`, `WrappedEEthChainlinkAdapter`, `RsEthChainlinkAdapter`, `BeHypeChainlinkAdapter`, `MantleLspStakingChainlinkAdapter`, `SyrupChainlinkAdapter`, `YInjChainlinkAdapter` +- Cross-format bridges: `DataFeedToBandStdAdapter`, `CompositeDataFeedToBandStdAdapter` -Custom price aggregator compatible with Chainlink's AggregatorV3 interface. Used to publish mToken prices on-chain +#### Acre Adapter ([`contracts/misc/acre/AcreAdapter.sol`](contracts/misc/acre/AcreAdapter.sol)) -**Key Functions:** +Wrapper around Midas vaults so they can be used by the Acre protocol. -- `setRoundData()`- function to push the price on-chain -- `setRoundDataSafe()`- same as `setRoundData()` but also performs a deviation check by comparing current and new prices -- `latestRoundData()` - View function, returns latest submitted price with submission details (check [AggregatorV3Interface.sol](@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol)) - -**CustomAggregatorV3CompatibleFeed Variations:** +#### Cross-Chain Contracts (Axelar, LayerZero) -- CustomAggregatorV3CompatibleFeedGrowth ([`contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol`](contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol)) - includes a growth parameter that automatically increases the feed price over time - -## Crosschain Contracts (Axelar, LayerZero) - -### **LayerZero Integration** (`contracts/misc/layerzero/`) +**LayerZero Integration** (`contracts/misc/layerzero/`) Contracts for LayerZero cross-chain messaging: - `MidasLzMintBurnOFTAdapter.sol` - OFT adapter that uses native mint/burn on mTokens - `MidasLzVaultComposerSync.sol` - Vault composer for instant-only operations -### **Axelar Integration** (`contracts/misc/axelar/`) +**Axelar Integration** (`contracts/misc/axelar/`) Contracts for Axelar cross-chain functionality: - `MidasAxelarVaultExecutable.sol` - Executable contract for cross-chain operations -## Deployed Contract Addresses - -All deployed contract addresses are stored in [`config/constants/addresses.ts`](./config/constants/addresses.ts). - -The addresses are organized by network and token. For example: - -```typescript -main: { - accessControl: '0x0312A9D1Ff2372DDEdCBB21e4B6389aFc919aC4B', - timelock: '0xE3EEe3e0D2398799C884a47FC40C029C8e241852', - mTBILL: { - token: '0xDD629E5241CbC5919847783e6C96B2De4754e438', - depositVault: '0x99361435420711723aF805F08187c9E6bF796683', - redemptionVault: '0xF6e51d24F4793Ac5e71e0502213a9BBE3A6d4517', - // ... - }, - // ... -} -``` - ## Deployment -Please fefer to [this deployment README](./scripts/deploy/README.md) +Please refer to [this deployment README](./scripts/deploy/README.md) ## Upgradability @@ -391,3 +467,23 @@ yarn format:sol yarn format:ts:fix yarn format:sol:fix ``` + +## Deployed Contract Addresses + +All deployed contract addresses are stored in [`config/constants/addresses.ts`](./config/constants/addresses.ts). + +The addresses are organized by network and token. For example: + +```typescript +main: { + accessControl: '0x0312A9D1Ff2372DDEdCBB21e4B6389aFc919aC4B', + timelock: '0xE3EEe3e0D2398799C884a47FC40C029C8e241852', + mTBILL: { + token: '0xDD629E5241CbC5919847783e6C96B2De4754e438', + depositVault: '0x99361435420711723aF805F08187c9E6bF796683', + redemptionVault: '0xF6e51d24F4793Ac5e71e0502213a9BBE3A6d4517', + // ... + }, + // ... +} +``` diff --git a/config/constants/addresses.ts b/config/constants/addresses.ts index 9ab3bfb1..419a66a4 100644 --- a/config/constants/addresses.ts +++ b/config/constants/addresses.ts @@ -84,6 +84,9 @@ export type DataFeedAddresses = export type MidasAddresses = Partial> & { accessControl?: string; timelock?: string; + timelockManager?: string; + timelockController?: string; + pauseManager?: string; paymentTokens?: Partial>; }; @@ -1604,6 +1607,11 @@ export const midasAddressesPerNetwork: ConfigPerNetwork< }, }, sepolia: { + timelock: '0x74e0a55Ea3Db85F6106FFD69Ef7c9829fd130888', + pauseManager: '0xbf21e448410BAA6039e73033F530027143c0c280', + timelockManager: '0xc6Ad27a2446Aa6223512D9FF8A6f3440a20ccd12', + timelockController: '0x982550a433239C23BFe6C57005A7396D2Ed706d2', + accessControl: '0xbf25b58cB8DfaD688F7BcB2b87D71C23A6600AaC', paymentTokens: { usdc: { dataFeed: '0x0e0eb6cdad90174f1Db606EC186ddD0B5eD80847', @@ -1639,12 +1647,11 @@ export const midasAddressesPerNetwork: ConfigPerNetwork< }, }, mTBILL: { - dataFeed: '0x4E677F7FE252DE44682a913f609EA3eb6F29DC3E', + dataFeed: '0x9393811adDd7F7Ff60D1be11DbD29025A15bf630', customFeedGrowth: '0x1E2165801d84865587252155Fb4580381f7A3FC4', - depositVault: '0x1615cBC603192ae8A9FF20E98dd0e40a405d76e4', - redemptionVault: '0x2fD18B0878967E19292E9a8BF38Bb1415F6ad653', - redemptionVaultBuidl: '0x6B35F2E4C9D4c1da0eDaf7fd7Dc90D9bCa4b0873', token: '0xefED40D1eb1577d1073e9C4F277463486D39b084', + depositVault: '0x2Abf7B5766Fc75bdb99e7aD76d6B539D08F3f8E1', + redemptionVault: '0x4e830D858c253C81ACF225E7101DF820D0e98415', layerZero: { oft: '0x0Ca81704F5df52E06205fe427653e661a4b6043c', composers: { @@ -1701,8 +1708,8 @@ export const midasAddressesPerNetwork: ConfigPerNetwork< token: '0x19569a89fEf7276a7f5967b6F6910c0573616f07', customFeed: '0xb64C014307622eB15046C66fF71D04258F5963DC', dataFeed: '0xffd462e0602Dd9FF3F038fd4e77a533f8c474b65', - depositVault: '0x56814399caaEDCEE4F58D2e55DA058A81DDE744f', - redemptionVaultSwapper: '0xFeB770Ae942ef5ed377c6D4BbC50f9d3b25Cf69b', + depositVault: '0x95059CaD850a0531dd9b086F019C7f1ACB15955c', + redemptionVault: '0x95AACf1bAD48336B0a62E90C2799fA8623CD9128', }, mFONE: { token: '0x6Ee5Bcb946499a926332cdE1993986bE76BE58Ea', @@ -1774,8 +1781,6 @@ export const midasAddressesPerNetwork: ConfigPerNetwork< depositVault: '0x807f2CF75EC43b11De43a529A0Dd9FEF754a9801', redemptionVaultSwapper: '0x313C76eCd990B728681f29464978D5637Cb78164', }, - timelock: '0x74e0a55Ea3Db85F6106FFD69Ef7c9829fd130888', - accessControl: '0xbf25b58cB8DfaD688F7BcB2b87D71C23A6600AaC', }, arbitrumSepolia: { paymentTokens: { diff --git a/contracts/DepositVault.sol b/contracts/DepositVault.sol index 9103a391..4c24da3c 100644 --- a/contracts/DepositVault.sol +++ b/contracts/DepositVault.sol @@ -1,15 +1,14 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import {IERC20MetadataUpgradeable as IERC20Metadata} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; +import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import {Counters} from "@openzeppelin/contracts/utils/Counters.sol"; +import {IDepositVault, DepositVaultInitParams, Request} from "./interfaces/IDepositVault.sol"; +import {CommonVaultInitParams, RequestStatus} from "./interfaces/IManageableVault.sol"; -import "./interfaces/IDepositVault.sol"; -import "./interfaces/IDataFeed.sol"; - -import "./abstract/ManageableVault.sol"; +import {ManageableVault} from "./abstract/ManageableVault.sol"; +import {Greenlistable} from "./access/Greenlistable.sol"; /** * @title DepositVault @@ -17,7 +16,7 @@ import "./abstract/ManageableVault.sol"; * @author RedDuck Software */ contract DepositVault is ManageableVault, IDepositVault { - using Counters for Counters.Counter; + using SafeERC20 for IERC20; /** * @notice return data of _calcAndValidateDeposit @@ -41,36 +40,15 @@ contract DepositVault is ManageableVault, IDepositVault { } /** - * @dev default role that grants admin rights to the contract - */ - bytes32 private constant _DEFAULT_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @dev selector for deposit instant - */ - bytes4 private constant _DEPOSIT_INSTANT_SELECTOR = - bytes4(keccak256("depositInstant(address,uint256,uint256,bytes32)")); - - /** - * @dev selector for deposit instant with custom recipient + * @notice request data storage */ - bytes4 private constant _DEPOSIT_INSTANT_WITH_CUSTOM_RECIPIENT_SELECTOR = - bytes4( - keccak256("depositInstant(address,uint256,uint256,bytes32,address)") - ); - - /** - * @dev selector for deposit request - */ - bytes4 private constant _DEPOSIT_REQUEST_SELECTOR = - bytes4(keccak256("depositRequest(address,uint256,bytes32)")); + mapping(uint256 => Request) public mintRequests; /** - * @dev selector for deposit request with custom recipient + * @dev how much mTokens were minted by the depositor + * @dev depositor address => amount minted */ - bytes4 private constant _DEPOSIT_REQUEST_WITH_CUSTOM_RECIPIENT_SELECTOR = - bytes4(keccak256("depositRequest(address,uint256,bytes32,address)")); + mapping(address => uint256) public totalMinted; /** * @notice minimal USD amount for first user`s deposit @@ -78,110 +56,53 @@ contract DepositVault is ManageableVault, IDepositVault { uint256 public minMTokenAmountForFirstDeposit; /** - * @notice mapping, requestId => request data + * @notice max supply cap value in mToken + * @dev if after the deposit, mToken.totalSupply() > maxSupplyCap, + * the tx will be reverted */ - mapping(uint256 => Request) public mintRequests; + uint256 public maxSupplyCap; /** - * @dev depositor address => amount minted + * @notice max amount per request in mToken */ - mapping(address => uint256) public totalMinted; + uint256 public maxAmountPerRequest; /** - * @notice max supply cap value in mToken - * @dev if after the deposit, mToken.totalSupply() > maxSupplyCap, - * the tx will be reverted + * @notice pending supply in mToken that will be released + * after the deposit request is processed */ - uint256 public maxSupplyCap; + uint256 public upcomingSupply; /** * @dev leaving a storage gap for futures updates - * - * used slots: - * 50 - `maxSupplyCap` */ - uint256[49] private __gap; + uint256[50] private __gap; + + /** + * @notice Passes role identifiers to the base ManageableVault constructor + * @param _contractAdminRole contract admin role identifier + * @param _greenlistedRole greenlisted role identifier + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(bytes32 _contractAdminRole, bytes32 _greenlistedRole) + ManageableVault(_contractAdminRole, _greenlistedRole) + {} /** * @notice upgradeable pattern contract`s initializer - * @dev Calls all versioned initializers (V1, V2, ...) in chronological order. - * This ensures that every deployment, whether fresh or upgraded, ends up - * initialized to the latest contract state without breaking the - * initializer/reinitializer versioning rules. - * @param _ac address of MidasAccessControll contract - * @param _mTokenInitParams init params for mToken - * @param _receiversInitParams init params for receivers - * @param _instantInitParams init params for instant operations - * @param _sanctionsList address of sanctionsList contract - * @param _variationTolerance percent of prices diviation 1% = 100 - * @param _minAmount basic min amount for operations in mToken - * @param _minMTokenAmountForFirstDeposit min amount for first deposit in mToken - * @param _maxSupplyCap max supply cap for mToken + * @param _commonVaultInitParams init params for common vault + * @param _depositVaultInitParams init params for deposit vault */ function initialize( - address _ac, - MTokenInitParams calldata _mTokenInitParams, - ReceiversInitParams calldata _receiversInitParams, - InstantInitParams calldata _instantInitParams, - address _sanctionsList, - uint256 _variationTolerance, - uint256 _minAmount, - uint256 _minMTokenAmountForFirstDeposit, - uint256 _maxSupplyCap - ) public { - initializeV1( - _ac, - _mTokenInitParams, - _receiversInitParams, - _instantInitParams, - _sanctionsList, - _variationTolerance, - _minAmount, - _minMTokenAmountForFirstDeposit - ); - - initializeV2(_maxSupplyCap); - } - - /** - * @notice v1 initializer - * @param _ac address of MidasAccessControll contract - * @param _mTokenInitParams init params for mToken - * @param _receiversInitParams init params for receivers - * @param _instantInitParams init params for instant operations - * @param _sanctionsList address of sanctionsList contract - * @param _variationTolerance percent of prices diviation 1% = 100 - * @param _minAmount basic min amount for operations in mToken - * @param _minMTokenAmountForFirstDeposit min amount for first deposit in mToken - */ - function initializeV1( - address _ac, - MTokenInitParams calldata _mTokenInitParams, - ReceiversInitParams calldata _receiversInitParams, - InstantInitParams calldata _instantInitParams, - address _sanctionsList, - uint256 _variationTolerance, - uint256 _minAmount, - uint256 _minMTokenAmountForFirstDeposit + CommonVaultInitParams calldata _commonVaultInitParams, + DepositVaultInitParams calldata _depositVaultInitParams ) public initializer { - __ManageableVault_init( - _ac, - _mTokenInitParams, - _receiversInitParams, - _instantInitParams, - _sanctionsList, - _variationTolerance, - _minAmount - ); - minMTokenAmountForFirstDeposit = _minMTokenAmountForFirstDeposit; - } + __ManageableVault_init(_commonVaultInitParams); - /** - * @notice v2 initializer - * @param _maxSupplyCap max supply cap for mToken - */ - function initializeV2(uint256 _maxSupplyCap) public reinitializer(2) { - maxSupplyCap = _maxSupplyCap; + minMTokenAmountForFirstDeposit = _depositVaultInitParams + .minMTokenAmountForFirstDeposit; + maxSupplyCap = _depositVaultInitParams.maxSupplyCap; + maxAmountPerRequest = _depositVaultInitParams.maxAmountPerRequest; } /** @@ -192,25 +113,21 @@ contract DepositVault is ManageableVault, IDepositVault { uint256 amountToken, uint256 minReceiveAmount, bytes32 referrerId - ) external whenFnNotPaused(_DEPOSIT_INSTANT_SELECTOR) { - _validateUserAccess(msg.sender); - - CalcAndValidateDepositResult memory result = _depositInstant( - tokenIn, - amountToken, - minReceiveAmount, - msg.sender - ); - - emit DepositInstant( - msg.sender, - tokenIn, - result.tokenAmountInUsd, - amountToken, - result.feeTokenAmount, - result.mintAmount, - referrerId - ); + ) + external + returns ( + uint256 /* mintAmount */ + ) + { + return + _depositInstantWithCustomRecipient( + tokenIn, + amountToken, + minReceiveAmount, + referrerId, + msg.sender, + ONE_HUNDRED_PERCENT + ).mintAmount; } /** @@ -224,31 +141,19 @@ contract DepositVault is ManageableVault, IDepositVault { address recipient ) external - whenFnNotPaused(_DEPOSIT_INSTANT_WITH_CUSTOM_RECIPIENT_SELECTOR) + returns ( + uint256 /* mintAmount */ + ) { - _validateUserAccess(msg.sender); - - if (recipient != msg.sender) { - _validateUserAccess(recipient); - } - - CalcAndValidateDepositResult memory result = _depositInstant( - tokenIn, - amountToken, - minReceiveAmount, - recipient - ); - - emit DepositInstantWithCustomRecipient( - msg.sender, - tokenIn, - recipient, - result.tokenAmountInUsd, - amountToken, - result.feeTokenAmount, - result.mintAmount, - referrerId - ); + return + _depositInstantWithCustomRecipient( + tokenIn, + amountToken, + minReceiveAmount, + referrerId, + recipient, + ONE_HUNDRED_PERCENT + ).mintAmount; } /** @@ -258,32 +163,16 @@ contract DepositVault is ManageableVault, IDepositVault { address tokenIn, uint256 amountToken, bytes32 referrerId - ) - external - whenFnNotPaused(_DEPOSIT_REQUEST_SELECTOR) - returns ( - uint256 /*requestId*/ - ) - { - _validateUserAccess(msg.sender); - - ( - uint256 requestId, - CalcAndValidateDepositResult memory calcResult - ) = _depositRequest(tokenIn, amountToken, msg.sender); - - emit DepositRequest( - requestId, - msg.sender, + ) external returns (uint256 requestId) { + (requestId, ) = _depositRequestWithCustomRecipient( tokenIn, amountToken, - calcResult.tokenAmountInUsd, - calcResult.feeTokenAmount, - calcResult.tokenOutRate, - referrerId + referrerId, + msg.sender, + 0, + 0, + msg.sender ); - - return requestId; } /** @@ -293,40 +182,27 @@ contract DepositVault is ManageableVault, IDepositVault { address tokenIn, uint256 amountToken, bytes32 referrerId, - address recipient + address recipientRequest, + uint256 instantShare, + uint256 minReceiveAmountInstantShare, + address recipientInstant ) external - whenFnNotPaused(_DEPOSIT_REQUEST_WITH_CUSTOM_RECIPIENT_SELECTOR) returns ( - uint256 /*requestId*/ + uint256, /*requestId*/ + uint256 /* instantMintAmount */ ) { - _validateUserAccess(msg.sender); - - if (recipient != msg.sender) { - _validateUserAccess(recipient); - } - - ( - uint256 requestId, - CalcAndValidateDepositResult memory calcResult - ) = _depositRequest(tokenIn, amountToken, recipient); - - bytes32 referrerIdCopy = referrerId; - - emit DepositRequestWithCustomRecipient( - requestId, - msg.sender, - tokenIn, - recipient, - amountToken, - calcResult.tokenAmountInUsd, - calcResult.feeTokenAmount, - calcResult.tokenOutRate, - referrerIdCopy - ); - - return requestId; + return + _depositRequestWithCustomRecipient( + tokenIn, + amountToken, + referrerId, + recipientRequest, + instantShare, + minReceiveAmountInstantShare, + recipientInstant + ); } /** @@ -334,18 +210,9 @@ contract DepositVault is ManageableVault, IDepositVault { */ function safeBulkApproveRequestAtSavedRate(uint256[] calldata requestIds) external - onlyVaultAdmin + onlyContractAdmin { - for (uint256 i = 0; i < requestIds.length; i++) { - uint256 rate = mintRequests[requestIds[i]].tokenOutRate; - bool success = _approveRequest(requestIds[i], rate, true, false); - - if (!success) { - continue; - } - - emit SafeApproveRequest(requestIds[i], rate); - } + _safeBulkApproveRequest(requestIds, 0, true, false); } /** @@ -353,48 +220,67 @@ contract DepositVault is ManageableVault, IDepositVault { */ function safeBulkApproveRequest(uint256[] calldata requestIds) external { uint256 currentMTokenRate = _getMTokenRate(); - safeBulkApproveRequest(requestIds, currentMTokenRate); + _safeBulkApproveRequest(requestIds, currentMTokenRate, false, false); } /** * @inheritdoc IDepositVault */ - function safeApproveRequest(uint256 requestId, uint256 newOutRate) + function safeBulkApproveRequestAvgRate(uint256[] calldata requestIds) external - onlyVaultAdmin { - _approveRequest(requestId, newOutRate, true, true); + uint256 currentMTokenRate = _getMTokenRate(); + _safeBulkApproveRequest(requestIds, currentMTokenRate, false, true); + } - emit SafeApproveRequest(requestId, newOutRate); + /** + * @inheritdoc IDepositVault + */ + function safeBulkApproveRequest( + uint256[] calldata requestIds, + uint256 newOutRate + ) external { + _safeBulkApproveRequest(requestIds, newOutRate, false, false); } /** * @inheritdoc IDepositVault */ - function approveRequest(uint256 requestId, uint256 newOutRate) - external - onlyVaultAdmin - { - _approveRequest(requestId, newOutRate, false, true); + function safeBulkApproveRequestAvgRate( + uint256[] calldata requestIds, + uint256 avgMTokenRate + ) external { + _safeBulkApproveRequest(requestIds, avgMTokenRate, false, true); + } - emit ApproveRequest(requestId, newOutRate); + /** + * @inheritdoc IDepositVault + */ + function approveRequest( + uint256 requestId, + uint256 newOutRate, + bool isAvgRate + ) external onlyContractAdmin { + _approveRequest(requestId, newOutRate, false, isAvgRate); } /** * @inheritdoc IDepositVault */ - function rejectRequest(uint256 requestId) external onlyVaultAdmin { + function rejectRequest(uint256 requestId) external onlyContractAdmin { Request memory request = mintRequests[requestId]; - require(request.sender != address(0), "DV: request not exist"); - require( - request.status == RequestStatus.Pending, - "DV: request not pending" - ); + _validateRequest(requestId, request.recipient, request.status); + _validateAndUpdateNextRequestIdToProcess(requestId, true); mintRequests[requestId].status = RequestStatus.Canceled; - emit RejectRequest(requestId, request.sender); + upcomingSupply -= _quoteMTokenFromRequest( + request, + request.tokenOutRate + ); + + emit RejectRequest(requestId); } /** @@ -402,63 +288,116 @@ contract DepositVault is ManageableVault, IDepositVault { */ function setMinMTokenAmountForFirstDeposit(uint256 newValue) external - onlyVaultAdmin + onlyContractAdmin { minMTokenAmountForFirstDeposit = newValue; - emit SetMinMTokenAmountForFirstDeposit(msg.sender, newValue); + emit SetMinMTokenAmountForFirstDeposit(newValue); } /** * @inheritdoc IDepositVault */ - function setMaxSupplyCap(uint256 newValue) external onlyVaultAdmin { + function setMaxSupplyCap(uint256 newValue) external onlyContractAdmin { maxSupplyCap = newValue; - emit SetMaxSupplyCap(msg.sender, newValue); + emit SetMaxSupplyCap(newValue); } /** * @inheritdoc IDepositVault */ - function safeBulkApproveRequest( + function setMaxAmountPerRequest(uint256 newValue) + external + onlyContractAdmin + { + maxAmountPerRequest = newValue; + + emit SetMaxAmountPerRequest(newValue); + } + + /** + * @notice calculates effective mToken supply including upcoming supply + * @return effective mToken supply + */ + function getEffectiveMTokenSupply() external view returns (uint256) { + return _getEffectiveMTokenSupply(); + } + + /** + * @dev internal function to approve requests + * @param requestIds request ids + * @param newOutRate new out rate + * @param isRequestRate if true, newOutRate will be ignored and request rate will be used + * @param isAvgRate if true, newOutRate is avg rate + */ + function _safeBulkApproveRequest( uint256[] calldata requestIds, - uint256 newOutRate - ) public onlyVaultAdmin { - for (uint256 i = 0; i < requestIds.length; i++) { + uint256 newOutRate, + bool isRequestRate, + bool isAvgRate + ) private onlyContractAdmin { + for (uint256 i = 0; i < requestIds.length; ++i) { + if (isRequestRate) { + newOutRate = mintRequests[requestIds[i]].tokenOutRate; + } bool success = _approveRequest( requestIds[i], newOutRate, true, - false + isAvgRate ); if (!success) { continue; } - - emit SafeApproveRequest(requestIds[i], newOutRate); } } /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure virtual override returns (bytes32) { - return _DEFAULT_DEPOSIT_VAULT_ADMIN_ROLE; - } - - /** - * @inheritdoc Greenlistable + * @dev internal deposit instant logic with custom recipient + * @param tokenIn tokenIn address + * @param amountToken amount of tokenIn (decimals 18) + * @param minReceiveAmount min amount of mToken to receive (decimals 18) + * @param referrerId referrer id + * @param recipient recipient address + * @param instantShareToValidate % amount of `amountToken` that will be deposited instantly */ - function greenlistTogglerRole() - public - view - virtual - override - returns (bytes32) + function _depositInstantWithCustomRecipient( + address tokenIn, + uint256 amountToken, + uint256 minReceiveAmount, + bytes32 referrerId, + address recipient, + uint256 instantShareToValidate + ) + private + validateUserAccess(recipient) + returns (CalcAndValidateDepositResult memory result) { - return vaultRole(); + require( + instantShareToValidate <= maxInstantShare, + InstantShareTooHigh(instantShareToValidate, maxInstantShare) + ); + + result = _depositInstant( + tokenIn, + amountToken, + minReceiveAmount, + recipient + ); + + emit DepositInstant( + msg.sender, + tokenIn, + recipient, + amountToken, + result.feeTokenAmount, + result.mintAmount, + result.tokenOutRate, + result.tokenInRate, + referrerId + ); } /** @@ -480,10 +419,7 @@ contract DepositVault is ManageableVault, IDepositVault { result = _calcAndValidateDeposit(user, tokenIn, amountToken, true); - require( - result.mintAmount >= minReceiveAmount, - "DV: minReceiveAmount > actual" - ); + _requireSlippageNotExceeded(result.mintAmount, minReceiveAmount); totalMinted[user] += result.mintAmount; @@ -491,73 +427,155 @@ contract DepositVault is ManageableVault, IDepositVault { _instantTransferTokensToTokensReceiver( tokenIn, - result.amountTokenWithoutFee, + result.amountTokenWithoutFee + result.feeTokenAmount, result.tokenDecimals ); - if (result.feeTokenAmount > 0) - _tokenTransferFromUser( - tokenIn, - feeReceiver, - result.feeTokenAmount, - result.tokenDecimals - ); - mToken.mint(recipient, result.mintAmount); _validateMaxSupplyCap(true); } /** - * @dev internal deposit request logic + * @dev internal deposit request logic with custom recipient * @param tokenIn tokenIn address * @param amountToken amount of tokenIn (decimals 18) - * @param recipient recipient address - * + * @param referrerId referrer id + * @param recipientRequest recipient address for the request part + * @param instantShare % amount of `amountToken` that will be deposited instantly + * @param minReceiveAmountInstantShare min amount of mToken to receive for the instant share + * @param recipientInstant recipient address for the instant part * @return requestId request id - * @return calcResult calculated deposit result */ - function _depositRequest( + function _depositRequestWithCustomRecipient( address tokenIn, uint256 amountToken, - address recipient + bytes32 referrerId, + address recipientRequest, + uint256 instantShare, + uint256 minReceiveAmountInstantShare, + address recipientInstant ) private + validateUserAccess(recipientRequest) returns ( - uint256 requestId, - CalcAndValidateDepositResult memory calcResult + uint256, /* requestId */ + uint256 /* instantMintAmount */ ) { + uint256 amountTokenInstant = _truncate( + (amountToken * instantShare) / ONE_HUNDRED_PERCENT, + _tokenDecimals(tokenIn) + ); + + CalcAndValidateDepositResult memory instantResult; + if (amountTokenInstant > 0) { + instantResult = _depositInstantWithCustomRecipient( + tokenIn, + amountTokenInstant, + minReceiveAmountInstantShare, + referrerId, + recipientInstant, + instantShare + ); + } + + uint256 amountTokenRequest = amountToken - amountTokenInstant; + + return ( + _depositRequest( + tokenIn, + amountTokenRequest, + recipientRequest, + referrerId, + amountTokenInstant, + instantResult.tokenAmountInUsd + ), + instantResult.mintAmount + ); + } + + /** + * @dev internal deposit request logic + * @param tokenIn tokenIn address + * @param amountToken amount of tokenIn (decimals 18) + * @param recipient recipient address + * @param referrerId referrer id + * @param depositedInstantAmount amount of tokenIn that was deposited instantly (decimals 18) + * @param depositedInstantUsdAmount amount of tokenIn that was deposited instantly in USD + + * @return requestId request id + */ + function _depositRequest( + address tokenIn, + uint256 amountToken, + address recipient, + bytes32 referrerId, + uint256 depositedInstantAmount, + uint256 depositedInstantUsdAmount + ) private returns (uint256 requestId) { address user = msg.sender; - requestId = currentRequestId.current(); - currentRequestId.increment(); + requestId = currentRequestId++; - calcResult = _calcAndValidateDeposit(user, tokenIn, amountToken, false); + CalcAndValidateDepositResult + memory calcResult = _calcAndValidateDeposit( + user, + tokenIn, + amountToken, + false + ); _requestTransferTokensToTokensReceiver( tokenIn, - calcResult.amountTokenWithoutFee, + calcResult.amountTokenWithoutFee + calcResult.feeTokenAmount, calcResult.tokenDecimals ); - if (calcResult.feeTokenAmount > 0) - _tokenTransferFromUser( - tokenIn, - feeReceiver, - calcResult.feeTokenAmount, - calcResult.tokenDecimals + // prevents stack too deep error + { + Request memory request = Request({ + recipient: recipient, + tokenIn: tokenIn, + status: RequestStatus.Pending, + depositedUsdAmount: calcResult.tokenAmountInUsd, + usdAmountWithoutFees: (calcResult.amountTokenWithoutFee * + calcResult.tokenInRate) / 10**18, + tokenOutRate: calcResult.tokenOutRate, + depositedInstantUsdAmount: depositedInstantUsdAmount, + approvedTokenOutRate: 0, + amountMToken: 0 + }); + + mintRequests[requestId] = request; + + uint256 estimatedMintAmount = _quoteMTokenFromRequest( + request, + request.tokenOutRate + ); + + require( + estimatedMintAmount <= maxAmountPerRequest, + MaxAmountPerRequestExceeded(estimatedMintAmount) ); - mintRequests[requestId] = Request({ - sender: recipient, - tokenIn: tokenIn, - status: RequestStatus.Pending, - depositedUsdAmount: calcResult.tokenAmountInUsd, - usdAmountWithoutFees: (calcResult.amountTokenWithoutFee * - calcResult.tokenInRate) / 10**18, - tokenOutRate: calcResult.tokenOutRate - }); + upcomingSupply += estimatedMintAmount; + } + + _validateMaxSupplyCap(true); + + emit DepositRequest( + requestId, + msg.sender, + tokenIn, + recipient, + amountToken, + depositedInstantAmount, + calcResult.feeTokenAmount, + calcResult.tokenOutRate, + calcResult.tokenInRate, + referrerId + ); } /** @@ -566,12 +584,21 @@ contract DepositVault is ManageableVault, IDepositVault { * Mints mTokens to user * @param requestId request id * @param newOutRate mToken rate + * @param isSafe if true: + * - safely validates max approve request id + * - safely validates if request id is sequential + * - safely validates max supply cap + * - requires variation tolerance + * @param isAvgRate if true, newOutRate is avg rate + * + * @return success true if success, otherwise false if isSafe flag is true, + * or revert if isSafe flag is false */ function _approveRequest( uint256 requestId, uint256 newOutRate, bool isSafe, - bool revertAboveSupplyCap + bool isAvgRate ) private returns ( @@ -580,30 +607,57 @@ contract DepositVault is ManageableVault, IDepositVault { { Request memory request = mintRequests[requestId]; - require(request.sender != address(0), "DV: request not exist"); - require( - request.status == RequestStatus.Pending, - "DV: request not pending" - ); + _validateRequest(requestId, request.recipient, request.status); + _validateUserAccess(request.recipient, false); - if (isSafe) + if (isSafe) { _requireVariationTolerance(request.tokenOutRate, newOutRate); + } + + if (isAvgRate) { + uint256 avgRate = _calculateHoldbackPartRateFromAvg( + request, + newOutRate + ); + + if (avgRate != 0) { + newOutRate = avgRate; + } + } + + require(newOutRate > 0, InvalidNewMTokenRate()); - uint256 amountMToken = (request.usdAmountWithoutFees * (10**18)) / - newOutRate; + uint256 amountMToken = _quoteMTokenFromRequest(request, newOutRate); - if (!_validateMaxSupplyCap(amountMToken, revertAboveSupplyCap)) { + uint256 upcomingSupplyDecrease = _quoteMTokenFromRequest( + request, + request.tokenOutRate + ); + + if ( + !_validateMaxSupplyCap( + upcomingSupplyDecrease, + amountMToken, + !isSafe + ) || !_validateAndUpdateNextRequestIdToProcess(requestId, !isSafe) + ) { return false; } - mToken.mint(request.sender, amountMToken); + upcomingSupply -= upcomingSupplyDecrease; + + mToken.mint(request.recipient, amountMToken); - totalMinted[request.sender] += amountMToken; + totalMinted[request.recipient] += amountMToken; + request.approvedTokenOutRate = newOutRate; + request.amountMToken = amountMToken; request.status = RequestStatus.Processed; - request.tokenOutRate = newOutRate; + mintRequests[requestId] = request; + emit ApproveRequest(requestId, newOutRate, isSafe, isAvgRate); + return true; } @@ -645,6 +699,26 @@ contract DepositVault is ManageableVault, IDepositVault { ); } + /** + * @notice validates request + * if exist + * if status is expected + * @param requestId request id + * @param validateAddress address to check if not zero + * @param status request status + */ + function _validateRequest( + uint256 requestId, + address validateAddress, + RequestStatus status + ) internal pure { + require(validateAddress != address(0), RequestNotExists(requestId)); + require( + status == RequestStatus.Pending, + UnexpectedRequestStatus(requestId, status) + ); + } + /** * @dev validate deposit and calculate mint amount * @param user user address @@ -660,7 +734,9 @@ contract DepositVault is ManageableVault, IDepositVault { uint256 amountToken, bool isInstant ) internal returns (CalcAndValidateDepositResult memory result) { - require(amountToken > 0, "DV: invalid amount"); + require(amountToken > 0, InvalidAmount()); + + _validateInstantFee(); result.tokenDecimals = _tokenDecimals(tokenIn); @@ -677,9 +753,10 @@ contract DepositVault is ManageableVault, IDepositVault { _requireAndUpdateAllowance(tokenIn, amountToken); result.feeTokenAmount = _truncate( - _getFeeAmount(userCopy, tokenIn, amountToken, isInstant, 0), + _getFeeAmount(_getFee(userCopy, tokenIn, isInstant), amountToken), result.tokenDecimals ); + result.amountTokenWithoutFee = amountToken - result.feeTokenAmount; uint256 feeInUsd = (result.feeTokenAmount * result.tokenInRate) / @@ -691,30 +768,18 @@ contract DepositVault is ManageableVault, IDepositVault { result.mintAmount = mTokenAmount; result.tokenOutRate = mTokenRate; - if (!isFreeFromMinAmount[userCopy]) { - _validateMinAmount(userCopy, result.mintAmount); + if ( + !_validateMTokenAmount(userCopy, result.mintAmount) && + totalMinted[userCopy] == 0 + ) { + require( + result.mintAmount >= minMTokenAmountForFirstDeposit, + LessThanMinAmountFirstDeposit( + result.mintAmount, + minMTokenAmountForFirstDeposit + ) + ); } - require(result.mintAmount > 0, "DV: invalid mint amount"); - } - - /** - * @dev validates that inputted USD amount >= minAmountToDepositInUsd() - * and amount >= minAmount() - * @param user user address - * @param amountMTokenWithoutFee amount of mToken without fee (decimals 18) - */ - function _validateMinAmount(address user, uint256 amountMTokenWithoutFee) - internal - view - { - require(amountMTokenWithoutFee >= minAmount, "DV: mToken amount < min"); - - if (totalMinted[user] != 0) return; - - require( - amountMTokenWithoutFee >= minMTokenAmountForFirstDeposit, - "DV: mint amount < min" - ); } /** @@ -730,30 +795,34 @@ contract DepositVault is ManageableVault, IDepositVault { view returns (bool) { - return _validateMaxSupplyCap(0, revertOnError); + return _validateMaxSupplyCap(0, 0, revertOnError); } /** * @dev validates that mToken.totalSupply() <= maxSupplyCap * + * @param requestEstimatedMintAmount estimated amount of mToken to mint from request * @param mintAmount amount of mToken to mint * @param revertOnError if true, will revert if supply is exceeded * if false, will return false if supply is exceeded without reverting * * @return true if supply is valid, false otherwise */ - function _validateMaxSupplyCap(uint256 mintAmount, bool revertOnError) - internal - view - returns (bool) - { - bool isExceeded = mToken.totalSupply() + mintAmount > maxSupplyCap; + function _validateMaxSupplyCap( + uint256 requestEstimatedMintAmount, + uint256 mintAmount, + bool revertOnError + ) private view returns (bool) { + bool isExceeded = _getEffectiveMTokenSupply() + + mintAmount - + requestEstimatedMintAmount > + maxSupplyCap; if (!revertOnError) { return !isExceeded; } - require(!isExceeded, "DV: max supply cap exceeded"); + require(!isExceeded, SupplyCapExceeded()); return true; } @@ -772,12 +841,7 @@ contract DepositVault is ManageableVault, IDepositVault { virtual returns (uint256 amountInUsd, uint256 rate) { - require(amount > 0, "DV: amount zero"); - - TokenConfig storage tokenConfig = tokensConfig[tokenIn]; - - rate = _getTokenRate(tokenConfig.dataFeed, tokenConfig.stable); - require(rate > 0, "DV: rate zero"); + rate = _getPTokenRate(tokenIn); amountInUsd = (amount * rate) / (10**18); } @@ -801,11 +865,58 @@ contract DepositVault is ManageableVault, IDepositVault { } /** - * @dev gets and validates mToken rate - * @return mTokenRate mToken rate + * @dev calculates holdback part rate from avg rate + * @param request request + * @param avgMTokenRate avg mToken rate + * @return holdback part rate + */ + function _calculateHoldbackPartRateFromAvg( + Request memory request, + uint256 avgMTokenRate + ) internal pure returns (uint256) { + if ( + avgMTokenRate == 0 || + request.tokenOutRate == 0 || + request.depositedInstantUsdAmount == 0 + ) { + return 0; + } + + uint256 targetTotalMTokenValue = ((request.depositedUsdAmount + + request.depositedInstantUsdAmount) * (10**18)) / avgMTokenRate; + + uint256 instantPartMTokenValue = (request.depositedInstantUsdAmount * + (10**18)) / request.tokenOutRate; + + if (targetTotalMTokenValue <= instantPartMTokenValue) { + return 0; + } + + uint256 holdbackPartValue = targetTotalMTokenValue - + instantPartMTokenValue; + + return (request.depositedUsdAmount * (10**18)) / holdbackPartValue; + } + + /** + * @dev calculates mToken amount to mint from request and provided mToken rate + * @param request request + * @param mTokenRate mToken rate + * @return mToken amount to mint + */ + function _quoteMTokenFromRequest(Request memory request, uint256 mTokenRate) + private + view + returns (uint256) + { + return (request.usdAmountWithoutFees * (10**18)) / mTokenRate; + } + + /** + * @dev calculates effective mToken supply including upcoming supply + * @return effective mToken supply */ - function _getMTokenRate() private view returns (uint256 mTokenRate) { - mTokenRate = _getTokenRate(address(mTokenDataFeed), false); - require(mTokenRate > 0, "DV: rate zero"); + function _getEffectiveMTokenSupply() private view returns (uint256) { + return mToken.totalSupply() + upcomingSupply; } } diff --git a/contracts/DepositVaultWithAave.sol b/contracts/DepositVaultWithAave.sol index 834f59d4..ea51a9bd 100644 --- a/contracts/DepositVaultWithAave.sol +++ b/contracts/DepositVaultWithAave.sol @@ -1,11 +1,14 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import "./DepositVault.sol"; -import "./interfaces/aave/IAaveV3Pool.sol"; +import {DepositVault} from "./DepositVault.sol"; +import {IAaveV3Pool} from "./interfaces/aave/IAaveV3Pool.sol"; +import {DecimalsCorrectionLibrary} from "./libraries/DecimalsCorrectionLibrary.sol"; +import {Greenlistable} from "./access/Greenlistable.sol"; +import {ManageableVault} from "./abstract/ManageableVault.sol"; /** * @title DepositVaultWithAave @@ -42,22 +45,16 @@ contract DepositVaultWithAave is DepositVault { /** * @notice Emitted when an Aave V3 Pool is configured for a payment token - * @param caller address of the caller * @param token payment token address * @param pool Aave V3 Pool address */ - event SetAavePool( - address indexed caller, - address indexed token, - address indexed pool - ); + event SetAavePool(address indexed token, address indexed pool); /** * @notice Emitted when an Aave V3 Pool is removed for a payment token - * @param caller address of the caller * @param token payment token address */ - event RemoveAavePool(address indexed caller, address indexed token); + event RemoveAavePool(address indexed token); /** * @notice Emitted when `aaveDepositsEnabled` flag is updated @@ -71,6 +68,35 @@ contract DepositVaultWithAave is DepositVault { */ event SetAutoInvestFallbackEnabled(bool indexed enabled); + /** + * @notice when token is not in pool + * @param aavePool Aave V3 Pool address + * @param token token address + */ + error TokenNotInPool(address aavePool, address token); + + /** + * @notice when pool is not set + * @param token token address + */ + error PoolNotSet(address token); + + /** + * @notice when auto-invest fails + * @param err error bytes + */ + error AutoInvestFailed(bytes err); + + /** + * @notice Passes role identifiers to the base DepositVault constructor + * @param _contractAdminRole contract admin role identifier + * @param _greenlistedRole greenlisted role identifier + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(bytes32 _contractAdminRole, bytes32 _greenlistedRole) + DepositVault(_contractAdminRole, _greenlistedRole) + {} + /** * @notice Sets the Aave V3 Pool for a specific payment token * @param _token payment token address @@ -78,33 +104,33 @@ contract DepositVaultWithAave is DepositVault { */ function setAavePool(address _token, address _aavePool) external - onlyVaultAdmin + onlyContractAdmin { _validateAddress(_token, true); _validateAddress(_aavePool, true); require( IAaveV3Pool(_aavePool).getReserveAToken(_token) != address(0), - "DVA: token not in pool" + TokenNotInPool(_aavePool, _token) ); aavePools[_token] = IAaveV3Pool(_aavePool); - emit SetAavePool(msg.sender, _token, _aavePool); + emit SetAavePool(_token, _aavePool); } /** * @notice Removes the Aave V3 Pool for a specific payment token * @param _token payment token address */ - function removeAavePool(address _token) external onlyVaultAdmin { - require(address(aavePools[_token]) != address(0), "DVA: pool not set"); + function removeAavePool(address _token) external onlyContractAdmin { + require(address(aavePools[_token]) != address(0), PoolNotSet(_token)); delete aavePools[_token]; - emit RemoveAavePool(msg.sender, _token); + emit RemoveAavePool(_token); } /** * @notice Updates `aaveDepositsEnabled` value * @param enabled whether Aave auto-invest deposits are enabled */ - function setAaveDepositsEnabled(bool enabled) external onlyVaultAdmin { + function setAaveDepositsEnabled(bool enabled) external onlyContractAdmin { aaveDepositsEnabled = enabled; emit SetAaveDepositsEnabled(enabled); } @@ -115,7 +141,7 @@ contract DepositVaultWithAave is DepositVault { */ function setAutoInvestFallbackEnabled(bool enabled) external - onlyVaultAdmin + onlyContractAdmin { autoInvestFallbackEnabled = enabled; emit SetAutoInvestFallbackEnabled(enabled); @@ -128,7 +154,7 @@ contract DepositVaultWithAave is DepositVault { address tokenIn, uint256 amountToken, uint256 tokensDecimals - ) internal override { + ) internal virtual override { IAaveV3Pool pool = aavePools[tokenIn]; if (!aaveDepositsEnabled || address(pool) == address(0)) { return @@ -149,7 +175,7 @@ contract DepositVaultWithAave is DepositVault { address tokenIn, uint256 amountToken, uint256 tokensDecimals - ) internal override { + ) internal virtual override { IAaveV3Pool pool = aavePools[tokenIn]; if (!aaveDepositsEnabled || address(pool) == address(0)) { return @@ -189,12 +215,12 @@ contract DepositVaultWithAave is DepositVault { try pool.supply(tokenIn, transferredAmount, tokensReceiver, 0) - {} catch { + {} catch (bytes memory error) { if (autoInvestFallbackEnabled) { IERC20(tokenIn).safeApprove(address(pool), 0); IERC20(tokenIn).safeTransfer(tokensReceiver, transferredAmount); } else { - revert("DVA: auto-invest failed"); + revert AutoInvestFailed(error); } } } diff --git a/contracts/DepositVaultWithMToken.sol b/contracts/DepositVaultWithMToken.sol index fb60093e..c9ec852b 100644 --- a/contracts/DepositVaultWithMToken.sol +++ b/contracts/DepositVaultWithMToken.sol @@ -1,11 +1,14 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import "./DepositVault.sol"; -import "./interfaces/IDepositVault.sol"; +import {DepositVault} from "./DepositVault.sol"; +import {ManageableVault} from "./abstract/ManageableVault.sol"; +import {IDepositVault, DepositVaultInitParams} from "./interfaces/IDepositVault.sol"; +import {CommonVaultInitParams} from "./interfaces/IManageableVault.sol"; +import {DecimalsCorrectionLibrary} from "./libraries/DecimalsCorrectionLibrary.sol"; /** * @title DepositVaultWithMToken @@ -42,13 +45,9 @@ contract DepositVaultWithMToken is DepositVault { /** * @notice Emitted when the mToken DepositVault address is updated - * @param caller address of the caller * @param newVault new mToken DepositVault address */ - event SetMTokenDepositVault( - address indexed caller, - address indexed newVault - ); + event SetMTokenDepositVault(address indexed newVault); /** * @notice Emitted when `mTokenDepositsEnabled` flag is updated @@ -62,42 +61,40 @@ contract DepositVaultWithMToken is DepositVault { */ event SetAutoInvestFallbackEnabled(bool indexed enabled); + /** + * @notice when zero mToken is received + * @param mTokenReceived mToken received + */ + error ZeroMTokenReceived(uint256 mTokenReceived); + + /** + * @notice when auto-invest fails + * @param err error bytes + */ + error AutoInvestFailed(bytes err); + + /** + * @notice Passes role identifiers to the base DepositVault constructor + * @param _contractAdminRole contract admin role identifier + * @param _greenlistedRole greenlisted role identifier + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(bytes32 _contractAdminRole, bytes32 _greenlistedRole) + DepositVault(_contractAdminRole, _greenlistedRole) + {} + /** * @notice upgradeable pattern contract`s initializer - * @param _ac address of MidasAccessControll contract - * @param _mTokenInitParams init params for mToken - * @param _receiversInitParams init params for receivers - * @param _instantInitParams init params for instant operations - * @param _sanctionsList address of sanctionsList contract - * @param _variationTolerance percent of prices diviation 1% = 100 - * @param _minAmount basic min amount for operations in mToken - * @param _minMTokenAmountForFirstDeposit min amount for first deposit in mToken - * @param _maxSupplyCap max supply cap for mToken + * @param _commonVaultInitParams init params for common vault + * @param _depositVaultInitParams init params for deposit vault * @param _mTokenDepositVault target mToken DepositVault address */ function initialize( - address _ac, - MTokenInitParams calldata _mTokenInitParams, - ReceiversInitParams calldata _receiversInitParams, - InstantInitParams calldata _instantInitParams, - address _sanctionsList, - uint256 _variationTolerance, - uint256 _minAmount, - uint256 _minMTokenAmountForFirstDeposit, - uint256 _maxSupplyCap, + CommonVaultInitParams calldata _commonVaultInitParams, + DepositVaultInitParams calldata _depositVaultInitParams, address _mTokenDepositVault ) external { - initialize( - _ac, - _mTokenInitParams, - _receiversInitParams, - _instantInitParams, - _sanctionsList, - _variationTolerance, - _minAmount, - _minMTokenAmountForFirstDeposit, - _maxSupplyCap - ); + initialize(_commonVaultInitParams, _depositVaultInitParams); _validateAddress(_mTokenDepositVault, true); mTokenDepositVault = IDepositVault(_mTokenDepositVault); @@ -109,22 +106,22 @@ contract DepositVaultWithMToken is DepositVault { */ function setMTokenDepositVault(address _mTokenDepositVault) external - onlyVaultAdmin + onlyContractAdmin { require( _mTokenDepositVault != address(mTokenDepositVault), - "DVMT: already set" + SameAddressValue(_mTokenDepositVault) ); _validateAddress(_mTokenDepositVault, true); mTokenDepositVault = IDepositVault(_mTokenDepositVault); - emit SetMTokenDepositVault(msg.sender, _mTokenDepositVault); + emit SetMTokenDepositVault(_mTokenDepositVault); } /** * @notice Updates `mTokenDepositsEnabled` value * @param enabled whether mToken auto-invest deposits are enabled */ - function setMTokenDepositsEnabled(bool enabled) external onlyVaultAdmin { + function setMTokenDepositsEnabled(bool enabled) external onlyContractAdmin { mTokenDepositsEnabled = enabled; emit SetMTokenDepositsEnabled(enabled); } @@ -135,7 +132,7 @@ contract DepositVaultWithMToken is DepositVault { */ function setAutoInvestFallbackEnabled(bool enabled) external - onlyVaultAdmin + onlyContractAdmin { autoInvestFallbackEnabled = enabled; emit SetAutoInvestFallbackEnabled(enabled); @@ -148,7 +145,7 @@ contract DepositVaultWithMToken is DepositVault { address tokenIn, uint256 amountToken, uint256 tokensDecimals - ) internal override { + ) internal virtual override { if (!mTokenDepositsEnabled) { return super._instantTransferTokensToTokensReceiver( @@ -168,7 +165,7 @@ contract DepositVaultWithMToken is DepositVault { address tokenIn, uint256 amountToken, uint256 tokensDecimals - ) internal override { + ) internal virtual override { if (!mTokenDepositsEnabled) { return super._requestTransferTokensToTokensReceiver( @@ -214,7 +211,6 @@ contract DepositVaultWithMToken is DepositVault { ); IERC20 targetMToken = IERC20(address(mTokenDepositVault.mToken())); - uint256 balanceBefore = targetMToken.balanceOf(address(this)); try mTokenDepositVault.depositInstant( @@ -223,17 +219,15 @@ contract DepositVaultWithMToken is DepositVault { 0, bytes32(0) ) - { - uint256 mTokenReceived = targetMToken.balanceOf(address(this)) - - balanceBefore; - require(mTokenReceived > 0, "DVMT: zero mToken received"); + returns (uint256 mTokenReceived) { + require(mTokenReceived > 0, ZeroMTokenReceived(mTokenReceived)); targetMToken.safeTransfer(tokensReceiver, mTokenReceived); - } catch { + } catch (bytes memory err) { if (autoInvestFallbackEnabled) { IERC20(tokenIn).safeApprove(address(mTokenDepositVault), 0); IERC20(tokenIn).safeTransfer(tokensReceiver, transferredAmount); } else { - revert("DVMT: auto-invest failed"); + revert AutoInvestFailed(err); } } } diff --git a/contracts/DepositVaultWithMorpho.sol b/contracts/DepositVaultWithMorpho.sol index d515da38..078ec1c5 100644 --- a/contracts/DepositVaultWithMorpho.sol +++ b/contracts/DepositVaultWithMorpho.sol @@ -1,10 +1,11 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import "./DepositVault.sol"; +import {DepositVault} from "./DepositVault.sol"; +import {DecimalsCorrectionLibrary} from "./libraries/DecimalsCorrectionLibrary.sol"; import {IMorphoVault} from "./interfaces/morpho/IMorphoVault.sol"; /** @@ -42,22 +43,16 @@ contract DepositVaultWithMorpho is DepositVault { /** * @notice Emitted when a Morpho Vault is configured for a payment token - * @param caller address of the caller * @param token payment token address * @param vault Morpho Vault address */ - event SetMorphoVault( - address indexed caller, - address indexed token, - address indexed vault - ); + event SetMorphoVault(address indexed token, address indexed vault); /** * @notice Emitted when a Morpho Vault is removed for a payment token - * @param caller address of the caller * @param token payment token address */ - event RemoveMorphoVault(address indexed caller, address indexed token); + event RemoveMorphoVault(address indexed token); /** * @notice Emitted when `morphoDepositsEnabled` flag is updated @@ -71,6 +66,41 @@ contract DepositVaultWithMorpho is DepositVault { */ event SetAutoInvestFallbackEnabled(bool indexed enabled); + /** + * @notice when asset mismatch + * @param morphoVault Morpho Vault address + * @param token token address + */ + error AssetMismatch(address morphoVault, address token); + + /** + * @notice when vault is not set + * @param token token address + */ + error VaultNotSet(address token); + + /** + * @notice when zero shares are received + * @param shares shares + */ + error ZeroShares(uint256 shares); + + /** + * @notice when auto-invest fails + * @param err error bytes + */ + error AutoInvestFailed(bytes err); + + /** + * @notice Passes role identifiers to the base DepositVault constructor + * @param _contractAdminRole contract admin role identifier + * @param _greenlistedRole greenlisted role identifier + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(bytes32 _contractAdminRole, bytes32 _greenlistedRole) + DepositVault(_contractAdminRole, _greenlistedRole) + {} + /** * @notice Sets the Morpho Vault for a specific payment token * @param _token payment token address @@ -78,36 +108,36 @@ contract DepositVaultWithMorpho is DepositVault { */ function setMorphoVault(address _token, address _morphoVault) external - onlyVaultAdmin + onlyContractAdmin { _validateAddress(_token, true); _validateAddress(_morphoVault, true); require( IMorphoVault(_morphoVault).asset() == _token, - "DVM: asset mismatch" + AssetMismatch(_morphoVault, _token) ); morphoVaults[_token] = IMorphoVault(_morphoVault); - emit SetMorphoVault(msg.sender, _token, _morphoVault); + emit SetMorphoVault(_token, _morphoVault); } /** * @notice Removes the Morpho Vault for a specific payment token * @param _token payment token address */ - function removeMorphoVault(address _token) external onlyVaultAdmin { + function removeMorphoVault(address _token) external onlyContractAdmin { require( address(morphoVaults[_token]) != address(0), - "DVM: vault not set" + VaultNotSet(_token) ); delete morphoVaults[_token]; - emit RemoveMorphoVault(msg.sender, _token); + emit RemoveMorphoVault(_token); } /** * @notice Updates `morphoDepositsEnabled` value * @param enabled whether Morpho auto-invest deposits are enabled */ - function setMorphoDepositsEnabled(bool enabled) external onlyVaultAdmin { + function setMorphoDepositsEnabled(bool enabled) external onlyContractAdmin { morphoDepositsEnabled = enabled; emit SetMorphoDepositsEnabled(enabled); } @@ -118,7 +148,7 @@ contract DepositVaultWithMorpho is DepositVault { */ function setAutoInvestFallbackEnabled(bool enabled) external - onlyVaultAdmin + onlyContractAdmin { autoInvestFallbackEnabled = enabled; emit SetAutoInvestFallbackEnabled(enabled); @@ -131,7 +161,7 @@ contract DepositVaultWithMorpho is DepositVault { address tokenIn, uint256 amountToken, uint256 tokensDecimals - ) internal override { + ) internal virtual override { IMorphoVault vault = morphoVaults[tokenIn]; if (!morphoDepositsEnabled || address(vault) == address(0)) { return @@ -152,7 +182,7 @@ contract DepositVaultWithMorpho is DepositVault { address tokenIn, uint256 amountToken, uint256 tokensDecimals - ) internal override { + ) internal virtual override { IMorphoVault vault = morphoVaults[tokenIn]; if (!morphoDepositsEnabled || address(vault) == address(0)) { return @@ -196,13 +226,13 @@ contract DepositVaultWithMorpho is DepositVault { try vault.deposit(transferredAmount, tokensReceiver) returns ( uint256 shares ) { - require(shares > 0, "DVM: zero shares"); - } catch { + require(shares > 0, ZeroShares(shares)); + } catch (bytes memory err) { if (autoInvestFallbackEnabled) { IERC20(tokenIn).safeApprove(address(vault), 0); IERC20(tokenIn).safeTransfer(tokensReceiver, transferredAmount); } else { - revert("DVM: auto-invest failed"); + revert AutoInvestFailed(err); } } } diff --git a/contracts/DepositVaultWithUSTB.sol b/contracts/DepositVaultWithUSTB.sol index 9ebbae5f..5a841b85 100644 --- a/contracts/DepositVaultWithUSTB.sol +++ b/contracts/DepositVaultWithUSTB.sol @@ -1,9 +1,13 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; +import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import {ISuperstateToken} from "./interfaces/ustb/ISuperstateToken.sol"; - -import "./DepositVault.sol"; +import {CommonVaultInitParams} from "./interfaces/IManageableVault.sol"; +import {DepositVault} from "./DepositVault.sol"; +import {DepositVaultInitParams} from "./interfaces/IDepositVault.sol"; +import {DecimalsCorrectionLibrary} from "./libraries/DecimalsCorrectionLibrary.sol"; /** * @title DepositVaultWithUSTB @@ -37,41 +41,40 @@ contract DepositVaultWithUSTB is DepositVault { */ event SetUstbDepositsEnabled(bool indexed enabled); + /** + * @notice when USTB token is not supported + * @param token token address + */ + error UnsupportedUSTBToken(address token); + + /** + * @notice when USTB fee is not zero + * @param fee fee + */ + error USTBFeeNotZero(uint256 fee); + + /** + * @notice Passes role identifiers to the base DepositVault constructor + * @param _contractAdminRole contract admin role identifier + * @param _greenlistedRole greenlisted role identifier + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(bytes32 _contractAdminRole, bytes32 _greenlistedRole) + DepositVault(_contractAdminRole, _greenlistedRole) + {} + /** * @notice upgradeable pattern contract`s initializer - * @param _ac address of MidasAccessControll contract - * @param _mTokenInitParams init params for mToken - * @param _receiversInitParams init params for receivers - * @param _instantInitParams init params for instant operations - * @param _sanctionsList address of sanctionsList contract - * @param _variationTolerance percent of prices diviation 1% = 100 - * @param _minAmount basic min amount for operations in mToken - * @param _minMTokenAmountForFirstDeposit min amount for first deposit in mToken + * @param _commonVaultInitParams init params for common vault + * @param _depositVaultInitParams init params for deposit vault * @param _ustb USTB token address */ function initialize( - address _ac, - MTokenInitParams calldata _mTokenInitParams, - ReceiversInitParams calldata _receiversInitParams, - InstantInitParams calldata _instantInitParams, - address _sanctionsList, - uint256 _variationTolerance, - uint256 _minAmount, - uint256 _minMTokenAmountForFirstDeposit, - uint256 _maxSupplyCap, + CommonVaultInitParams calldata _commonVaultInitParams, + DepositVaultInitParams calldata _depositVaultInitParams, address _ustb ) external { - initialize( - _ac, - _mTokenInitParams, - _receiversInitParams, - _instantInitParams, - _sanctionsList, - _variationTolerance, - _minAmount, - _minMTokenAmountForFirstDeposit, - _maxSupplyCap - ); + initialize(_commonVaultInitParams, _depositVaultInitParams); _validateAddress(_ustb, false); @@ -82,7 +85,7 @@ contract DepositVaultWithUSTB is DepositVault { * @notice Updates `ustbDepositsEnabled` value * @param enabled whether USTB deposits are enabled */ - function setUstbDepositsEnabled(bool enabled) external onlyVaultAdmin { + function setUstbDepositsEnabled(bool enabled) external onlyContractAdmin { ustbDepositsEnabled = enabled; emit SetUstbDepositsEnabled(enabled); } @@ -102,7 +105,7 @@ contract DepositVaultWithUSTB is DepositVault { address tokenIn, uint256 amountToken, uint256 tokensDecimals - ) internal override { + ) internal virtual override { if (!ustbDepositsEnabled) { return super._instantTransferTokensToTokensReceiver( @@ -117,10 +120,10 @@ contract DepositVaultWithUSTB is DepositVault { require( config.sweepDestination != address(0), - "DVU: unsupported USTB token" + UnsupportedUSTBToken(tokenIn) ); - require(config.fee == 0, "DVU: USTB fee is not 0"); + require(config.fee == 0, USTBFeeNotZero(config.fee)); address ustbToken = ustb; uint256 transferredAmount = _tokenTransferFromUser( diff --git a/contracts/RedemptionVault.sol b/contracts/RedemptionVault.sol index 8f5762a2..2540ef6d 100644 --- a/contracts/RedemptionVault.sol +++ b/contracts/RedemptionVault.sol @@ -1,17 +1,15 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import {IERC20MetadataUpgradeable as IERC20Metadata} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; +import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {Counters} from "@openzeppelin/contracts/utils/Counters.sol"; - -import "./interfaces/IRedemptionVault.sol"; -import "./interfaces/IDataFeed.sol"; - -import "./abstract/ManageableVault.sol"; - -import "./access/Greenlistable.sol"; +import {DecimalsCorrectionLibrary} from "./libraries/DecimalsCorrectionLibrary.sol"; +import {IRedemptionVault, LiquidityProviderLoanRequest, Request, RedemptionVaultInitParams} from "./interfaces/IRedemptionVault.sol"; +import {CommonVaultInitParams, RequestStatus} from "./interfaces/IManageableVault.sol"; +import {ManageableVault} from "./abstract/ManageableVault.sol"; +import {RedemptionSwapperHelpersLibrary} from "./libraries/RedemptionSwapperHelpersLibrary.sol"; /** * @title RedemptionVault @@ -20,143 +18,108 @@ import "./access/Greenlistable.sol"; */ contract RedemptionVault is ManageableVault, IRedemptionVault { using DecimalsCorrectionLibrary for uint256; - using Counters for Counters.Counter; + using SafeERC20 for IERC20; /** * @notice return data of _calcAndValidateRedeem * packed into a struct to avoid stack too deep errors */ struct CalcAndValidateRedeemResult { - /// @notice fee amount in mToken + /// @notice fee amount in paymentToken uint256 feeAmount; - /// @notice amount of mToken without fee - uint256 amountMTokenWithoutFee; + /// @notice amount of paymentToken without fee + uint256 amountTokenOutWithoutFee; + /// @notice amount of paymentToken with fee + uint256 amountTokenOut; + /// @notice payment token rate + uint256 tokenOutRate; + /// @notice mToken rate + uint256 mTokenRate; + /// @notice tokenOut decimals + uint256 tokenOutDecimals; } /** - * @dev default role that grants admin rights to the contract + * @notice mapping, requestId to request data */ - bytes32 private constant _DEFAULT_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("REDEMPTION_VAULT_ADMIN_ROLE"); + mapping(uint256 => Request) public redeemRequests; /** - * @dev selector for redeem instant + * @notice mapping, loanRequestId to loan request data */ - bytes4 private constant _REDEEM_INSTANT_SELECTOR = - bytes4(keccak256("redeemInstant(address,uint256,uint256)")); + mapping(uint256 => LiquidityProviderLoanRequest) public loanRequests; /** - * @dev selector for redeem instant with custom recipient + * @notice address is designated for standard redemptions, allowing tokens to be pulled from this address */ - bytes4 private constant _REDEEM_INSTANT_WITH_CUSTOM_RECIPIENT_SELECTOR = - bytes4(keccak256("redeemInstant(address,uint256,uint256,address)")); + address public requestRedeemer; /** - * @dev selector for redeem request + * @notice address of loan liquidity provider */ - bytes4 private constant _REDEEM_REQUEST_SELECTOR = - bytes4(keccak256("redeemRequest(address,uint256)")); + address public loanLp; /** - * @dev selector for redeem request with custom recipient + * @notice address from which payment tokens will be pulled during loan repayment */ - bytes4 private constant _REDEEM_REQUEST_WITH_CUSTOM_RECIPIENT_SELECTOR = - bytes4(keccak256("redeemRequest(address,uint256,address)")); + address public loanRepaymentAddress; /** - * @notice min amount for fiat requests + * @notice loan APR value in basis points (100 = 1%) */ - uint256 public minFiatRedeemAmount; + uint256 public loanApr; /** - * @notice fee percent for fiat requests + * @notice flag to determine if the loan LP liquidity should be used first */ - uint256 public fiatAdditionalFee; + bool public preferLoanLiquidity; /** - * @notice static fee in mToken for fiat requests + * @notice last loan request id */ - uint256 public fiatFlatFee; + uint256 public currentLoanRequestId; /** - * @notice mapping, requestId to request data + * @notice address of loan RedemptionVault-compatible vault */ - mapping(uint256 => Request) public redeemRequests; + IRedemptionVault public loanSwapperVault; /** - * @notice address is designated for standard redemptions, allowing tokens to be pulled from this address + * @dev leaving a storage gap for futures updates */ - address public requestRedeemer; + uint256[50] private __gap; /** - * @dev leaving a storage gap for futures updates + * @notice Passes role identifiers to the base ManageableVault constructor + * @param _contractAdminRole contract admin role identifier + * @param _greenlistedRole greenlisted role identifier + * @custom:oz-upgrades-unsafe-allow constructor */ - uint256[50] private __gap; + constructor(bytes32 _contractAdminRole, bytes32 _greenlistedRole) + ManageableVault(_contractAdminRole, _greenlistedRole) + {} /** * @notice upgradeable pattern contract`s initializer - * @param _ac address of MidasAccessControll contract - * @param _mTokenInitParams init params for mToken - * @param _receiversInitParams init params for receivers - * @param _instantInitParams init params for instant operations - * @param _sanctionsList address of sanctionsList contract - * @param _variationTolerance percent of prices diviation 1% = 100 - * @param _minAmount basic min amount for operations - * @param _fiatRedemptionInitParams params fiatAdditionalFee, fiatFlatFee, minFiatRedeemAmount - * @param _requestRedeemer address is designated for standard redemptions, allowing tokens to be pulled from this address + * @param _commonVaultInitParams init params for common vault + * @param _redemptionVaultInitParams init params for redemption vault */ function initialize( - address _ac, - MTokenInitParams calldata _mTokenInitParams, - ReceiversInitParams calldata _receiversInitParams, - InstantInitParams calldata _instantInitParams, - address _sanctionsList, - uint256 _variationTolerance, - uint256 _minAmount, - FiatRedeptionInitParams calldata _fiatRedemptionInitParams, - address _requestRedeemer - ) external initializer { - __RedemptionVault_init( - _ac, - _mTokenInitParams, - _receiversInitParams, - _instantInitParams, - _sanctionsList, - _variationTolerance, - _minAmount, - _fiatRedemptionInitParams, - _requestRedeemer - ); - } + CommonVaultInitParams calldata _commonVaultInitParams, + RedemptionVaultInitParams calldata _redemptionVaultInitParams + ) public initializer { + __ManageableVault_init(_commonVaultInitParams); - // solhint-disable func-name-mixedcase - function __RedemptionVault_init( - address _ac, - MTokenInitParams calldata _mTokenInitParams, - ReceiversInitParams calldata _receiversInitParams, - InstantInitParams calldata _instantInitParams, - address _sanctionsList, - uint256 _variationTolerance, - uint256 _minAmount, - FiatRedeptionInitParams calldata _fiatRedemptionInitParams, - address _requestRedeemer - ) internal onlyInitializing { - __ManageableVault_init( - _ac, - _mTokenInitParams, - _receiversInitParams, - _instantInitParams, - _sanctionsList, - _variationTolerance, - _minAmount - ); - _validateFee(_fiatRedemptionInitParams.fiatAdditionalFee, false); - _validateAddress(_requestRedeemer, false); + _validateAddress(_redemptionVaultInitParams.requestRedeemer, false); - minFiatRedeemAmount = _fiatRedemptionInitParams.minFiatRedeemAmount; - fiatAdditionalFee = _fiatRedemptionInitParams.fiatAdditionalFee; - fiatFlatFee = _fiatRedemptionInitParams.fiatFlatFee; - requestRedeemer = _requestRedeemer; + requestRedeemer = _redemptionVaultInitParams.requestRedeemer; + + loanLp = _redemptionVaultInitParams.loanLp; + loanRepaymentAddress = _redemptionVaultInitParams.loanRepaymentAddress; + loanSwapperVault = IRedemptionVault( + _redemptionVaultInitParams.loanSwapperVault + ); + loanApr = _redemptionVaultInitParams.loanApr; } /** @@ -166,26 +129,15 @@ contract RedemptionVault is ManageableVault, IRedemptionVault { address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount - ) external whenFnNotPaused(_REDEEM_INSTANT_SELECTOR) { - _validateUserAccess(msg.sender); - - ( - CalcAndValidateRedeemResult memory calcResult, - uint256 amountTokenOutWithoutFee - ) = _redeemInstant( + ) external returns (uint256) { + return + _redeemInstantWithCustomRecipient( tokenOut, amountMTokenIn, minReceiveAmount, - msg.sender + msg.sender, + ONE_HUNDRED_PERCENT ); - - emit RedeemInstant( - msg.sender, - tokenOut, - amountMTokenIn, - calcResult.feeAmount, - amountTokenOutWithoutFee - ); } /** @@ -196,31 +148,15 @@ contract RedemptionVault is ManageableVault, IRedemptionVault { uint256 amountMTokenIn, uint256 minReceiveAmount, address recipient - ) external whenFnNotPaused(_REDEEM_INSTANT_WITH_CUSTOM_RECIPIENT_SELECTOR) { - _validateUserAccess(msg.sender); - - if (recipient != msg.sender) { - _validateUserAccess(recipient); - } - - ( - CalcAndValidateRedeemResult memory calcResult, - uint256 amountTokenOutWithoutFee - ) = _redeemInstant( + ) external returns (uint256) { + return + _redeemInstantWithCustomRecipient( tokenOut, amountMTokenIn, minReceiveAmount, - recipient + recipient, + ONE_HUNDRED_PERCENT ); - - emit RedeemInstantWithCustomRecipient( - msg.sender, - tokenOut, - recipient, - amountMTokenIn, - calcResult.feeAmount, - amountTokenOutWithoutFee - ); } /** @@ -228,27 +164,16 @@ contract RedemptionVault is ManageableVault, IRedemptionVault { */ function redeemRequest(address tokenOut, uint256 amountMTokenIn) external - whenFnNotPaused(_REDEEM_REQUEST_SELECTOR) - returns ( - uint256 /*requestId*/ - ) + returns (uint256 requestId) { - _validateUserAccess(msg.sender); - - ( - uint256 requestId, - CalcAndValidateRedeemResult memory calcResult - ) = _redeemRequest(tokenOut, amountMTokenIn, false, msg.sender); - - emit RedeemRequest( - requestId, - msg.sender, + (requestId, ) = _redeemRequestWithCustomRecipient( tokenOut, amountMTokenIn, - calcResult.feeAmount + msg.sender, + 0, + 0, + msg.sender ); - - return requestId; } /** @@ -257,479 +182,937 @@ contract RedemptionVault is ManageableVault, IRedemptionVault { function redeemRequest( address tokenOut, uint256 amountMTokenIn, - address recipient + address recipientRequest, + uint256 instantShare, + uint256 minReceiveAmountInstantShare, + address recipientInstant ) external - whenFnNotPaused(_REDEEM_REQUEST_WITH_CUSTOM_RECIPIENT_SELECTOR) returns ( - uint256 /*requestId*/ + uint256, /*requestId*/ + uint256 /* instantReceivedAmount */ ) { - _validateUserAccess(msg.sender); - - if (recipient != msg.sender) { - _validateUserAccess(recipient); - } - - ( - uint256 requestId, - CalcAndValidateRedeemResult memory calcResult - ) = _redeemRequest(tokenOut, amountMTokenIn, false, recipient); - - emit RedeemRequestWithCustomRecipient( - requestId, - msg.sender, - tokenOut, - recipient, - amountMTokenIn, - calcResult.feeAmount - ); - - return requestId; + return + _redeemRequestWithCustomRecipient( + tokenOut, + amountMTokenIn, + recipientRequest, + instantShare, + minReceiveAmountInstantShare, + recipientInstant + ); } /** * @inheritdoc IRedemptionVault */ - function redeemFiatRequest(uint256 amountMTokenIn) + function safeBulkApproveRequestAtSavedRate(uint256[] calldata requestIds) external - whenFnNotPaused(this.redeemFiatRequest.selector) - returns ( - uint256 /*requestId*/ - ) + onlyContractAdmin { - _validateUserAccess(msg.sender); - - ( - uint256 requestId, - CalcAndValidateRedeemResult memory calcResult - ) = _redeemRequest( - MANUAL_FULLFILMENT_TOKEN, - amountMTokenIn, - true, - msg.sender - ); - - emit RedeemRequest( - requestId, - msg.sender, - MANUAL_FULLFILMENT_TOKEN, - amountMTokenIn, - calcResult.feeAmount - ); + _safeBulkApproveRequest(requestIds, 0, true, false); + } - return requestId; + /** + * @inheritdoc IRedemptionVault + */ + function safeBulkApproveRequest(uint256[] calldata requestIds) external { + _safeBulkApproveRequest(requestIds, _getMTokenRate(), false, false); } /** * @inheritdoc IRedemptionVault */ - function safeBulkApproveRequestAtSavedRate(uint256[] calldata requestIds) + function safeBulkApproveRequestAvgRate(uint256[] calldata requestIds) external - onlyVaultAdmin { - for (uint256 i = 0; i < requestIds.length; i++) { - uint256 rate = redeemRequests[requestIds[i]].mTokenRate; - bool success = _approveRequest(requestIds[i], rate, true, true); + _safeBulkApproveRequest(requestIds, _getMTokenRate(), false, true); + } - if (!success) { - continue; - } + /** + * @inheritdoc IRedemptionVault + */ + function safeBulkApproveRequest( + uint256[] calldata requestIds, + uint256 newOutRate + ) external { + _safeBulkApproveRequest(requestIds, newOutRate, false, false); + } - emit SafeApproveRequest(requestIds[i], rate); - } + /** + * @inheritdoc IRedemptionVault + */ + function safeBulkApproveRequestAvgRate( + uint256[] calldata requestIds, + uint256 avgMTokenRate + ) external { + _safeBulkApproveRequest(requestIds, avgMTokenRate, false, true); } /** * @inheritdoc IRedemptionVault */ - function safeBulkApproveRequest(uint256[] calldata requestIds) external { - uint256 currentMTokenRate = _getMTokenRate(); - safeBulkApproveRequest(requestIds, currentMTokenRate); + function approveRequest( + uint256 requestId, + uint256 newMTokenRate, + bool isAvgRate + ) external onlyContractAdmin { + _approveRequest(requestId, newMTokenRate, false, isAvgRate); } /** * @inheritdoc IRedemptionVault */ - function approveRequest(uint256 requestId, uint256 newMTokenRate) - external - onlyVaultAdmin - { - _approveRequest(requestId, newMTokenRate, false, false); + function rejectRequest(uint256 requestId) external onlyContractAdmin { + Request memory request = redeemRequests[requestId]; + + _validateRequest(requestId, request.recipient, request.status); + _validateAndUpdateNextRequestIdToProcess(requestId, true); - emit ApproveRequest(requestId, newMTokenRate); + redeemRequests[requestId].status = RequestStatus.Canceled; + + emit RejectRequest(requestId); } /** * @inheritdoc IRedemptionVault */ - function safeApproveRequest(uint256 requestId, uint256 newMTokenRate) + function bulkRepayLpLoanRequest(uint256[] calldata requestIds) external - onlyVaultAdmin + onlyContractAdmin { - _approveRequest(requestId, newMTokenRate, true, false); + uint256 _loanApr = loanApr; + for (uint256 i = 0; i < requestIds.length; ++i) { + LiquidityProviderLoanRequest memory request = loanRequests[ + requestIds[i] + ]; + + _validateRequest(requestIds[i], request.tokenOut, request.status); + + uint8 decimals = _tokenDecimals(request.tokenOut); + uint256 duration = block.timestamp - request.createdAt; + uint256 accruedInterest = _truncate( + (request.amountTokenOut * _loanApr * duration) / + (10_000 * 365 days), + decimals + ); - emit SafeApproveRequest(requestId, newMTokenRate); + uint256 amountFee; + + if (accruedInterest > request.amountFee) { + amountFee = accruedInterest; + loanRequests[requestIds[i]].amountFee = amountFee; + } else { + amountFee = request.amountFee; + } + + _tokenTransferFromTo( + request.tokenOut, + loanRepaymentAddress, + loanLp, + request.amountTokenOut + amountFee, + decimals + ); + + loanRequests[requestIds[i]].status = RequestStatus.Processed; + emit RepayLpLoanRequest(requestIds[i], amountFee); + } } /** * @inheritdoc IRedemptionVault */ - function rejectRequest(uint256 requestId) external onlyVaultAdmin { - Request memory request = redeemRequests[requestId]; + function cancelLpLoanRequest(uint256 requestId) external onlyContractAdmin { + LiquidityProviderLoanRequest memory request = loanRequests[requestId]; - _validateRequest(request.sender, request.status); - - redeemRequests[requestId].status = RequestStatus.Canceled; + _validateRequest(requestId, request.tokenOut, request.status); - emit RejectRequest(requestId, request.sender); + loanRequests[requestId].status = RequestStatus.Canceled; + emit CancelLpLoanRequest(requestId); } /** * @inheritdoc IRedemptionVault */ - function setMinFiatRedeemAmount(uint256 newValue) external onlyVaultAdmin { - minFiatRedeemAmount = newValue; + function setRequestRedeemer(address redeemer) external onlyContractAdmin { + _validateAddress(redeemer, false); + + requestRedeemer = redeemer; - emit SetMinFiatRedeemAmount(msg.sender, newValue); + emit SetRequestRedeemer(redeemer); } /** * @inheritdoc IRedemptionVault */ - function setFiatFlatFee(uint256 feeInMToken) external onlyVaultAdmin { - fiatFlatFee = feeInMToken; + function setLoanLp(address newLoanLp) external onlyContractAdmin { + loanLp = newLoanLp; - emit SetFiatFlatFee(msg.sender, feeInMToken); + emit SetLoanLp(newLoanLp); } /** * @inheritdoc IRedemptionVault */ - function setFiatAdditionalFee(uint256 newFee) external onlyVaultAdmin { - _validateFee(newFee, false); - - fiatAdditionalFee = newFee; + function setLoanRepaymentAddress(address newLoanRepaymentAddress) + external + onlyContractAdmin + { + loanRepaymentAddress = newLoanRepaymentAddress; - emit SetFiatAdditionalFee(msg.sender, newFee); + emit SetLoanRepaymentAddress(newLoanRepaymentAddress); } /** * @inheritdoc IRedemptionVault */ - function setRequestRedeemer(address redeemer) external onlyVaultAdmin { - _validateAddress(redeemer, false); + function setLoanSwapperVault(address newLoanSwapperVault) + external + onlyContractAdmin + { + loanSwapperVault = IRedemptionVault(newLoanSwapperVault); - requestRedeemer = redeemer; + emit SetLoanSwapperVault(newLoanSwapperVault); + } - emit SetRequestRedeemer(msg.sender, redeemer); + /** + * @inheritdoc IRedemptionVault + */ + function setLoanApr(uint256 newLoanApr) external onlyContractAdmin { + loanApr = newLoanApr; + + emit SetLoanApr(newLoanApr); } /** * @inheritdoc IRedemptionVault */ - function safeBulkApproveRequest( + function setPreferLoanLiquidity(bool newLoanLpFirst) + external + onlyContractAdmin + { + preferLoanLiquidity = newLoanLpFirst; + + emit SetPreferLoanLiquidity(newLoanLpFirst); + } + + /** + * @dev internal function to approve requests + * @param requestIds request ids + * @param newOutRate new out rate + * @param isRequestRate if true, newOutRate will be ignored and request rate will be used + * @param isAvgRate if true, newOutRate is avg rate + */ + function _safeBulkApproveRequest( uint256[] calldata requestIds, - uint256 newOutRate - ) public onlyVaultAdmin { - for (uint256 i = 0; i < requestIds.length; i++) { + uint256 newOutRate, + bool isRequestRate, + bool isAvgRate + ) private onlyContractAdmin { + for (uint256 i = 0; i < requestIds.length; ++i) { + if (isRequestRate) { + newOutRate = redeemRequests[requestIds[i]].mTokenRate; + } + bool success = _approveRequest( requestIds[i], newOutRate, true, - true + isAvgRate ); if (!success) { continue; } - - emit SafeApproveRequest(requestIds[i], newOutRate); } } - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure virtual override returns (bytes32) { - return _DEFAULT_REDEMPTION_VAULT_ADMIN_ROLE; - } - - /** - * @inheritdoc Greenlistable - */ - function greenlistTogglerRole() - public - view - virtual - override - returns (bytes32) - { - return vaultRole(); - } - /** * @dev validates approve * burns amount from contract - * transfer tokenOut to user if not fiat + * transfer tokenOut to user * sets flag Processed * @param requestId request id * @param newMTokenRate new mToken rate - * @param isSafe new mToken rate - * @param safeValidateLiquidity if true, checks if there is enough liquidity - * and if its not sufficient, function wont fail + * @param isSafe if true: + * - safely validates max approve request id + * - safely validates if request id is sequential + * - safely validates if there is enough liquidity + * - requires variation tolerance + * @param isAvgRate if true, calculates holdback part rate from avg rate * - * @return success true if success, false only in case if - * safeValidateLiquidity == true and there is not enough liquidity + * @return success true if success, otherwise false if isSafe flag is true, + * or revert if isSafe flag is false */ function _approveRequest( uint256 requestId, uint256 newMTokenRate, bool isSafe, - bool safeValidateLiquidity + bool isAvgRate ) - internal + private returns ( bool /* success */ ) { Request memory request = redeemRequests[requestId]; - _validateRequest(request.sender, request.status); + _validateRequest(requestId, request.recipient, request.status); + + _validateUserAccess(request.recipient, false); if (isSafe) { _requireVariationTolerance(request.mTokenRate, newMTokenRate); } - bool isFiat = request.tokenOut == MANUAL_FULLFILMENT_TOKEN; + if (isAvgRate) { + uint256 avgRate = _calculateHoldbackPartRateFromAvg( + request, + newMTokenRate + ); - uint256 tokenDecimals = isFiat ? 18 : _tokenDecimals(request.tokenOut); + if (avgRate != 0) { + newMTokenRate = avgRate; + } + } - uint256 amountTokenOutWithoutFee = _truncate( - (request.amountMToken * newMTokenRate) / request.tokenOutRate, - tokenDecimals - ); + require(newMTokenRate > 0, InvalidNewMTokenRate()); - if (!isFiat) { - if ( - safeValidateLiquidity && - !_validateLiquidity( - request.tokenOut, - amountTokenOutWithoutFee, - tokenDecimals - ) - ) { - return false; - } + CalcAndValidateRedeemResult memory calcResult = _calcAndValidateRedeem( + request.recipient, + request.tokenOut, + request.amountMToken, + newMTokenRate, + request.tokenOutRate, + true, + request.feePercent, + false + ); - _tokenTransferFromTo( - request.tokenOut, - requestRedeemer, - request.sender, - amountTokenOutWithoutFee, - tokenDecimals - ); + if ( + (isSafe && + IERC20(request.tokenOut).balanceOf(requestRedeemer) < + (calcResult.amountTokenOutWithoutFee + calcResult.feeAmount) + .convertFromBase18(calcResult.tokenOutDecimals)) || + !_validateAndUpdateNextRequestIdToProcess(requestId, !isSafe) + ) { + return false; } - _requireAndUpdateAllowance(request.tokenOut, amountTokenOutWithoutFee); + _tokenTransferFromTo( + request.tokenOut, + requestRedeemer, + request.recipient, + calcResult.amountTokenOutWithoutFee, + calcResult.tokenOutDecimals + ); + + _requireAndUpdateAllowance(request.tokenOut, calcResult.amountTokenOut); - mToken.burn(address(this), request.amountMToken); + mToken.burn(requestRedeemer, request.amountMToken); + request.amountTokenOut = calcResult.amountTokenOutWithoutFee; + request.approvedMTokenRate = newMTokenRate; request.status = RequestStatus.Processed; - request.mTokenRate = newMTokenRate; + redeemRequests[requestId] = request; + emit ApproveRequest(requestId, newMTokenRate, isSafe, isAvgRate); + return true; } /** * @notice validates request * if exist - * if not processed - * @param sender sender address - * @param status request status + * if status is expected + * @param requestId request id + * @param validateAddress address to check if not zero + * @param status actual request status */ - function _validateRequest(address sender, RequestStatus status) - internal - pure - { - require(sender != address(0), "RV: request not exist"); - require(status == RequestStatus.Pending, "RV: request not pending"); + function _validateRequest( + uint256 requestId, + address validateAddress, + RequestStatus status + ) private pure { + require(validateAddress != address(0), RequestNotExists(requestId)); + require( + status == RequestStatus.Pending, + UnexpectedRequestStatus(requestId, status) + ); } /** - * @dev internal redeem instant logic + * @dev internal redeem instant logic with custom recipient * @param tokenOut tokenOut address * @param amountMTokenIn amount of mToken (decimals 18) * @param minReceiveAmount min amount of tokenOut to receive (decimals 18) * @param recipient recipient address - * - * @return calcResult calculated redeem result - * @return amountTokenOutWithoutFee amount of tokenOut without fee + * @param instantShareToValidate % amount of instant share to validate */ - function _redeemInstant( + function _redeemInstantWithCustomRecipient( address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount, - address recipient + address recipient, + uint256 instantShareToValidate + ) private validateUserAccess(recipient) returns (uint256) { + require( + instantShareToValidate <= maxInstantShare, + InstantShareTooHigh(instantShareToValidate, maxInstantShare) + ); + + CalcAndValidateRedeemResult memory calcResult = _redeemInstant( + tokenOut, + amountMTokenIn, + minReceiveAmount, + recipient + ); + + // emitting earlier for easier loan matching on the indexer + emit RedeemInstant( + msg.sender, + tokenOut, + recipient, + amountMTokenIn, + calcResult.feeAmount, + calcResult.amountTokenOutWithoutFee, + calcResult.mTokenRate, + calcResult.tokenOutRate + ); + + _obtainLiquidityAndTransfer(tokenOut, recipient, calcResult); + + return + calcResult.amountTokenOutWithoutFee.convertFromBase18( + calcResult.tokenOutDecimals + ); + } + + /** + * @dev internal redeem request logic with custom recipient + * @param tokenOut tokenOut address + * @param amountMTokenIn amount of mToken (decimals 18) + * @param recipientRequest recipient address for the request part + * @param instantShare % amount of `amountMTokenIn` that will be redeemed instantly + * @param minReceiveAmountInstantShare min amount of tokenOut to receive for the instant share + * @param recipientInstant recipient address for the instant part + * @return requestId request id + */ + function _redeemRequestWithCustomRecipient( + address tokenOut, + uint256 amountMTokenIn, + address recipientRequest, + uint256 instantShare, + uint256 minReceiveAmountInstantShare, + address recipientInstant ) - internal - virtual + private + validateUserAccess(recipientRequest) returns ( - CalcAndValidateRedeemResult memory calcResult, - uint256 amountTokenOutWithoutFee + uint256, /* requestId */ + uint256 instantReceivedAmount ) { + uint256 amountMTokenInInstant = (amountMTokenIn * instantShare) / + ONE_HUNDRED_PERCENT; + + if (amountMTokenInInstant > 0) { + instantReceivedAmount = _redeemInstantWithCustomRecipient( + tokenOut, + amountMTokenInInstant, + minReceiveAmountInstantShare, + recipientInstant, + instantShare + ); + } + + uint256 amountMTokenInRequest = amountMTokenIn - amountMTokenInInstant; + + return ( + _redeemRequest( + tokenOut, + amountMTokenInRequest, + recipientRequest, + amountMTokenInInstant + ), + instantReceivedAmount + ); + } + + /** + * @dev internal redeem instant logic + * @param tokenOut tokenOut address + * @param amountMTokenIn amount of mToken (decimals 18) + * @param minReceiveAmount min amount of tokenOut to receive (decimals 18) + * + * @return calcResult calculated redeem result + */ + function _redeemInstant( + address tokenOut, + uint256 amountMTokenIn, + uint256 minReceiveAmount, + address /* recipient */ + ) private returns (CalcAndValidateRedeemResult memory calcResult) { address user = msg.sender; + _validateInstantFee(); + calcResult = _calcAndValidateRedeem( user, tokenOut, amountMTokenIn, - true, - false + 0, + 0, + false, + 0, + true ); _requireAndUpdateLimit(amountMTokenIn); - address tokenOutCopy = tokenOut; - uint256 tokenDecimals = _tokenDecimals(tokenOutCopy); - - (uint256 amountMTokenInUsd, uint256 mTokenRate) = _convertMTokenToUsd( - amountMTokenIn - ); - (uint256 amountTokenOut, uint256 tokenOutRate) = _convertUsdToToken( - amountMTokenInUsd, - tokenOutCopy + _requireSlippageNotExceeded( + calcResult.amountTokenOutWithoutFee, + minReceiveAmount ); - amountTokenOutWithoutFee = _truncate( - (calcResult.amountMTokenWithoutFee * mTokenRate) / tokenOutRate, - tokenDecimals - ); + _requireAndUpdateAllowance(tokenOut, calcResult.amountTokenOut); - require( - amountTokenOutWithoutFee >= minReceiveAmount, - "RV: minReceiveAmount > actual" - ); + mToken.burn(user, amountMTokenIn); + } - _requireAndUpdateAllowance(tokenOutCopy, amountTokenOut); + /** + * @dev Calculates how much of liquidity is needed to fulfill the redemption + * and gets missing amount from all the available sources - vault liquidity and loan LP liquidity + * if `preferLoanLiquidity` is true, it will first try to use loan LP liquidity, + */ + function _obtainLiquidity( + address tokenOut, + CalcAndValidateRedeemResult memory calcResult + ) private returns (uint256 usedLpLiquidity, uint256 lpFeePortion) { + uint256 tokenOutBalanceBase18 = IERC20(tokenOut) + .balanceOf(address(this)) + .convertToBase18(_tokenDecimals(tokenOut)); - mToken.burn(user, calcResult.amountMTokenWithoutFee); - if (calcResult.feeAmount > 0) - _tokenTransferFromUser( - address(mToken), - feeReceiver, + uint256 totalAmount = calcResult.amountTokenOutWithoutFee + + calcResult.feeAmount; + + if (preferLoanLiquidity) { + (usedLpLiquidity, lpFeePortion) = _tryObtainLoanLpLiquidity( + tokenOut, + totalAmount, + totalAmount, + calcResult.tokenOutRate, calcResult.feeAmount, - 18 + calcResult.tokenOutDecimals + ); + + uint256 newBalance = tokenOutBalanceBase18 + usedLpLiquidity; + + if (newBalance < totalAmount) { + _tryObtainVaultLiquidity( + tokenOut, + totalAmount - newBalance, + calcResult.tokenOutRate, + newBalance, + calcResult.tokenOutDecimals + ); + } + } else if (tokenOutBalanceBase18 < totalAmount) { + uint256 obtainedVaultLiquidity = _tryObtainVaultLiquidity( + tokenOut, + totalAmount - tokenOutBalanceBase18, + calcResult.tokenOutRate, + tokenOutBalanceBase18, + calcResult.tokenOutDecimals ); - _tokenTransferToUser( - tokenOutCopy, + uint256 newBalance = tokenOutBalanceBase18 + obtainedVaultLiquidity; + + if (newBalance < totalAmount) { + (usedLpLiquidity, lpFeePortion) = _tryObtainLoanLpLiquidity( + tokenOut, + totalAmount - newBalance, + totalAmount, + calcResult.tokenOutRate, + calcResult.feeAmount, + calcResult.tokenOutDecimals + ); + } + } + } + + /** + * @dev Obtains liquidity from different sources and transfers it to the recipient + * as well as fee distribution + * @param tokenOut tokenOut address + * @param recipient recipient address + * @param calcResult calculated redeem result + */ + function _obtainLiquidityAndTransfer( + address tokenOut, + address recipient, + CalcAndValidateRedeemResult memory calcResult + ) private { + uint256 usedLpLiquidity; + uint256 lpFeePortion; + + (usedLpLiquidity, lpFeePortion) = _obtainLiquidity( + tokenOut, + calcResult + ); + + // transfer from vault liquidity to user + _tokenTransferFromTo( + tokenOut, + address(this), recipient, - amountTokenOutWithoutFee, - tokenDecimals + calcResult.amountTokenOutWithoutFee, + calcResult.tokenOutDecimals + ); + + if (usedLpLiquidity == 0) { + return; + } + + uint256 loanRequestId = currentLoanRequestId++; + + loanRequests[loanRequestId] = LiquidityProviderLoanRequest({ + tokenOut: tokenOut, + amountTokenOut: usedLpLiquidity, + amountFee: lpFeePortion, + createdAt: block.timestamp, + status: RequestStatus.Pending + }); + + emit CreateLiquidityProviderLoanRequest( + loanRequestId, + tokenOut, + usedLpLiquidity, + lpFeePortion, + calcResult.mTokenRate, + calcResult.tokenOutRate ); } /** - * @notice internal redeem request logic + * @dev wraps _obtainVaultLiquidityExternal with try/catch * @param tokenOut tokenOut address - * @param amountMTokenIn amount of mToken (decimals 18) - * - * @return requestId request id - * @return calcResult calc result + * @param missingAmountBase18 amount of tokenOut needed in base 18 + * @param tokenOutRate tokenOut rate + * @param currentTokenOutBalanceBase18 current balance of tokenOut in the vault in base 18 + * @param tokenOutDecimals decimals of tokenOut */ - function _redeemRequest( + function _tryObtainVaultLiquidity( address tokenOut, - uint256 amountMTokenIn, - bool isFiat, - address recipient + uint256 missingAmountBase18, + uint256 tokenOutRate, + uint256 currentTokenOutBalanceBase18, + uint256 tokenOutDecimals + ) private returns (uint256 obtainedLiquidityBase18) { + try + this._obtainVaultLiquidityExternal( + tokenOut, + missingAmountBase18, + tokenOutRate, + currentTokenOutBalanceBase18, + tokenOutDecimals + ) + returns (uint256 _obtainedLiquidityBase18) { + obtainedLiquidityBase18 = _obtainedLiquidityBase18; + } catch { + // do nothing + } + } + + /** + * @dev wraps _obtainLoanLpLiquidityExternal with try/catch + * @param tokenOut tokenOut address + * @param missingAmountBase18 amount of tokenOut needed in base 18 + * @param totalAmount total amount of tokenOut needed in base 18 + * @param tokenOutRate tokenOut rate + * @param totalFee total fee of tokenOut + * @param tokenOutDecimals decimals of tokenOut + */ + function _tryObtainLoanLpLiquidity( + address tokenOut, + uint256 missingAmountBase18, + uint256 totalAmount, + uint256 tokenOutRate, + uint256 totalFee, + uint256 tokenOutDecimals + ) private returns (uint256 obtainedLiquidityBase18, uint256 lpFeePortion) { + try + this._obtainLoanLpLiquidityExternal( + tokenOut, + missingAmountBase18, + totalAmount, + tokenOutRate, + totalFee, + tokenOutDecimals + ) + returns (uint256 _obtainedLiquidityBase18, uint256 _lpFeePortion) { + (obtainedLiquidityBase18, lpFeePortion) = ( + _obtainedLiquidityBase18, + _lpFeePortion + ); + } catch { + // do nothing + } + } + + /** + * @dev Check if contract has enough tokenOut balance for redeem, + * if not, obtains liquidity trough the custom strategies. + * In default implementation it does nothing. + * @return obtainedLiquidityBase18 amount of tokenOut obtained + */ + function _obtainVaultLiquidity( + address, /* tokenOut */ + uint256, /* missingAmountBase18 */ + uint256, /* tokenOutRate */ + uint256, /* currentTokenOutBalanceBase18 */ + uint256 /* tokenOutDecimals */ ) internal + virtual + returns ( + uint256 /* obtainedLiquidityBase18 */ + ) + { + return 0; + } + + /** + * @notice This function can only be called by the contract itself (self-call restriction) + * @dev only calls _obtainVaultLiquidity internally and external because its used with try/catch + * @param tokenOut tokenOut address + * @param missingAmountBase18 amount of tokenOut needed in base 18 + * @param tokenOutRate tokenOut rate + * @param currentTokenOutBalanceBase18 current balance of tokenOut in the vault in base 18 + * @param tokenOutDecimals decimals of tokenOut + * @return obtainedLiquidityBase18 amount of tokenOut obtained + */ + // solhint-disable-next-line private-vars-leading-underscore + function _obtainVaultLiquidityExternal( + address tokenOut, + uint256 missingAmountBase18, + uint256 tokenOutRate, + uint256 currentTokenOutBalanceBase18, + uint256 tokenOutDecimals + ) + external returns ( - uint256 requestId, - CalcAndValidateRedeemResult memory calcResult + uint256 /* obtainedLiquidityBase18 */ ) { - if (!isFiat) { - require( - tokenOut != MANUAL_FULLFILMENT_TOKEN, - "RV: tokenOut == fiat" + _requireSelfCall(); + return + _obtainVaultLiquidity( + tokenOut, + missingAmountBase18, + tokenOutRate, + currentTokenOutBalanceBase18, + tokenOutDecimals ); + } + + /** + * @notice This function can only be called by the contract itself (self-call restriction) + * @dev Check if contract has enough tokenOut balance for redeem; + * if not, redeem the missing amount via loan LP liquidity + * @param tokenOut tokenOut address + * @param missingAmountBase18 amount of tokenOut needed in base 18 + * @param totalAmount total amount of tokenOut needed in base 18 + * @param tokenOutRate tokenOut rate + * @param totalFee total fee of tokenOut + * @param tokenOutDecimals decimals of tokenOut + */ + // solhint-disable-next-line private-vars-leading-underscore + function _obtainLoanLpLiquidityExternal( + address tokenOut, + uint256 missingAmountBase18, + uint256 totalAmount, + uint256 tokenOutRate, + uint256 totalFee, + uint256 tokenOutDecimals + ) + external + returns ( + uint256, /* amountReceivedBase18 */ + uint256 /* feePortionBase18 */ + ) + { + _requireSelfCall(); + + address _loanLp = loanLp; + IRedemptionVault _loanSwapperVault = loanSwapperVault; + + if (missingAmountBase18 == 0) { + return (0, 0); } - address user = msg.sender; + // loan lp is not configured + if (_loanLp == address(0) || address(_loanSwapperVault) == address(0)) { + return (0, 0); + } - calcResult = _calcAndValidateRedeem( - user, - tokenOut, - amountMTokenIn, - false, - isFiat - ); + if (!_loanSwapperVault.waivedFeeRestriction(address(this))) { + return (0, 0); + } + + uint256 mTokenARate; + IERC20 mTokenA; + uint256 grossTokenOutAmount; + + // prevent stack too deep errors + { + uint256 mTokenABalance; + + ( + mTokenARate, + mTokenA, + mTokenABalance + ) = RedemptionSwapperHelpersLibrary.getSwapperDetails( + _loanSwapperVault, + _loanLp + ); + + grossTokenOutAmount = Math.mulDiv( + mTokenABalance, + mTokenARate, + tokenOutRate, + Math.Rounding.Up + ); + } + + if (grossTokenOutAmount > missingAmountBase18) { + grossTokenOutAmount = missingAmountBase18; + } - address tokenOutCopy = tokenOut; + if (grossTokenOutAmount == 0) { + return (0, 0); + } - // assigning the default value which is gonna be used - // only for fiat redemptions - uint256 tokenOutRate = 1e18; + uint256 lpFeePortion = _truncate( + (totalFee * grossTokenOutAmount) / totalAmount, + tokenOutDecimals + ); - if (!isFiat) { - TokenConfig storage config = tokensConfig[tokenOutCopy]; - tokenOutRate = _getTokenRate(config.dataFeed, config.stable); + if (grossTokenOutAmount == lpFeePortion) { + return (0, lpFeePortion); } - uint256 mTokenRate = mTokenDataFeed.getDataInBase18(); + address _tokenOut = tokenOut; + uint256 _tokenOutDecimals = tokenOutDecimals; + + // Ceil so the inner vault's floored output is still >= net token out amount. + // Requires address(this) to have waivedFeeRestriction on the inner vault. + uint256 mTokenAAmount = Math.mulDiv( + grossTokenOutAmount - lpFeePortion, + tokenOutRate, + mTokenARate, + Math.Rounding.Up + ); + + return ( + RedemptionSwapperHelpersLibrary.redeemInstantSwapper( + _loanSwapperVault, + mTokenA, + _loanLp, + _tokenOut, + mTokenAAmount, + _tokenOutDecimals + ), + lpFeePortion + ); + } + + /** + * @notice internal redeem request logic + * @param tokenOut tokenOut address + * @param amountMTokenIn amount of mToken (decimals 18) + * @param recipient recipient address + * @param amountMTokenInstant amount of mToken that was redeemed instantly + * + * @return requestId request id + */ + function _redeemRequest( + address tokenOut, + uint256 amountMTokenIn, + address recipient, + uint256 amountMTokenInstant + ) private returns (uint256 requestId) { + _requireTokenExists(tokenOut); + + address user = msg.sender; + + _validateMTokenAmount(user, amountMTokenIn); + + _validateInstantFee(); + + (, uint256 mTokenRate, uint256 tokenOutRate) = _convertMTokenToTokenOut( + amountMTokenIn, + 0, + tokenOut, + 0 + ); _tokenTransferFromUser( address(mToken), - address(this), - calcResult.amountMTokenWithoutFee, + address(requestRedeemer), + amountMTokenIn, 18 // mToken always have 18 decimals ); - if (calcResult.feeAmount > 0) - _tokenTransferFromUser( - address(mToken), - feeReceiver, - calcResult.feeAmount, - 18 - ); - requestId = currentRequestId.current(); - currentRequestId.increment(); + requestId = currentRequestId++; + + uint256 feePercent = _getFee(user, tokenOut, false); redeemRequests[requestId] = Request({ - sender: recipient, - tokenOut: tokenOutCopy, + recipient: recipient, + tokenOut: tokenOut, status: RequestStatus.Pending, - amountMToken: calcResult.amountMTokenWithoutFee, + amountMToken: amountMTokenIn, mTokenRate: mTokenRate, - tokenOutRate: tokenOutRate + tokenOutRate: tokenOutRate, + feePercent: feePercent, + amountMTokenInstant: amountMTokenInstant, + approvedMTokenRate: 0, + amountTokenOut: 0 }); - return (requestId, calcResult); + emit RedeemRequest( + requestId, + msg.sender, + tokenOut, + recipient, + amountMTokenIn, + amountMTokenInstant, + feePercent, + mTokenRate, + tokenOutRate + ); } /** * @dev calculates tokenOut amount from USD amount * @param amountUsd amount of USD (decimals 18) * @param tokenOut tokenOut address - * + * @param overrideTokenRate override token rate if not zero + * @return amountToken converted USD to tokenOut * @return tokenRate conversion rate */ - function _convertUsdToToken(uint256 amountUsd, address tokenOut) - internal - view - returns (uint256 amountToken, uint256 tokenRate) - { - require(amountUsd > 0, "RV: amount zero"); - - TokenConfig storage tokenConfig = tokensConfig[tokenOut]; - - tokenRate = _getTokenRate(tokenConfig.dataFeed, tokenConfig.stable); - require(tokenRate > 0, "RV: rate zero"); + function _convertUsdToToken( + uint256 amountUsd, + address tokenOut, + uint256 overrideTokenRate + ) internal view returns (uint256 amountToken, uint256 tokenRate) { + tokenRate = overrideTokenRate > 0 + ? overrideTokenRate + : _getPTokenRate(tokenOut); amountToken = (amountUsd * (10**18)) / tokenRate; } @@ -737,18 +1120,18 @@ contract RedemptionVault is ManageableVault, IRedemptionVault { /** * @dev calculates USD amount from mToken amount * @param amountMToken amount of mToken (decimals 18) + * @param overrideTokenRate override mToken rate if not zero * * @return amountUsd converted amount to USD * @return mTokenRate conversion rate */ - function _convertMTokenToUsd(uint256 amountMToken) - internal - view - returns (uint256 amountUsd, uint256 mTokenRate) - { - require(amountMToken > 0, "RV: amount zero"); - - mTokenRate = _getMTokenRate(); + function _convertMTokenToUsd( + uint256 amountMToken, + uint256 overrideTokenRate + ) internal view returns (uint256 amountUsd, uint256 mTokenRate) { + mTokenRate = overrideTokenRate > 0 + ? overrideTokenRate + : _getMTokenRate(); amountUsd = (amountMToken * mTokenRate) / (10**18); } @@ -758,8 +1141,11 @@ contract RedemptionVault is ManageableVault, IRedemptionVault { * @param user user address * @param tokenOut tokenOut address * @param amountMTokenIn mToken amount (decimals 18) + * @param overrideMTokenRate override mToken rate if not zero + * @param overrideTokenOutRate override token rate if not zero + * @param shouldOverrideFeePercent should override fee percent if true + * @param overrideFeePercent override fee percent if shouldOverrideFeePercent is true * @param isInstant is instant operation - * @param isFiat is fiat operation * * @return result calc result */ @@ -767,68 +1153,125 @@ contract RedemptionVault is ManageableVault, IRedemptionVault { address user, address tokenOut, uint256 amountMTokenIn, - bool isInstant, - bool isFiat - ) internal view returns (CalcAndValidateRedeemResult memory result) { - require(amountMTokenIn > 0, "RV: invalid amount"); - - if (!isFreeFromMinAmount[user]) { - uint256 minRedeemAmount = isFiat ? minFiatRedeemAmount : minAmount; - require(minRedeemAmount <= amountMTokenIn, "RV: amount < min"); - } + uint256 overrideMTokenRate, + uint256 overrideTokenOutRate, + bool shouldOverrideFeePercent, + uint256 overrideFeePercent, + bool isInstant + ) + internal + view + virtual + returns (CalcAndValidateRedeemResult memory result) + { + _requireTokenExists(tokenOut); + + _validateMTokenAmount(user, amountMTokenIn); + + ( + uint256 amountTokenOut, + uint256 mTokenRate, + uint256 tokenOutRate + ) = _convertMTokenToTokenOut( + amountMTokenIn, + overrideMTokenRate, + tokenOut, + overrideTokenOutRate + ); + + result.tokenOutDecimals = _tokenDecimals(tokenOut); + result.tokenOutRate = tokenOutRate; + result.mTokenRate = mTokenRate; result.feeAmount = _getFeeAmount( - user, - tokenOut, - amountMTokenIn, - isInstant, - isFiat ? fiatAdditionalFee : 0 + shouldOverrideFeePercent + ? overrideFeePercent + : _getFee(user, tokenOut, isInstant), + amountTokenOut ); - if (isFiat) { - require( - tokenOut == MANUAL_FULLFILMENT_TOKEN, - "RV: tokenOut != fiat" - ); - if (!waivedFeeRestriction[user]) result.feeAmount += fiatFlatFee; - } else { - _requireTokenExists(tokenOut); - } + amountTokenOut = _truncate(amountTokenOut, result.tokenOutDecimals); + result.feeAmount = _truncate(result.feeAmount, result.tokenOutDecimals); - require(amountMTokenIn > result.feeAmount, "RV: amountMTokenIn < fee"); + require( + amountTokenOut > result.feeAmount, + FeeExceedsAmount(result.feeAmount, amountTokenOut) + ); - result.amountMTokenWithoutFee = amountMTokenIn - result.feeAmount; + result.amountTokenOut = amountTokenOut; + + result.amountTokenOutWithoutFee = amountTokenOut - result.feeAmount; } - /* - * @dev validates that liquidity of provided token on `requestRedeemer` is enough - * @param token token address - * @param requiredLiquidity minimum required liquidity of `requestRedeemer` - * @param tokenDecimals `token` decimals + /** + * @dev converts mToken to tokenOut amount + * @param amountMTokenIn amount of mToken + * @param overrideMTokenRate override mToken rate if not zero + * @param tokenOut tokenOut address + * @param overrideTokenOutRate override token rate if not zero * - * @return false if not enough liquidity, otherwise true + * @return amountTokenOut amount of tokenOut + * @return mTokenRate conversion rate + * @return tokenOutRate conversion rate */ - function _validateLiquidity( - address token, - uint256 requiredLiquidity, - uint256 tokenDecimals + function _convertMTokenToTokenOut( + uint256 amountMTokenIn, + uint256 overrideMTokenRate, + address tokenOut, + uint256 overrideTokenOutRate ) - internal + private view returns ( - bool /* success */ + uint256, + uint256, + uint256 ) { - uint256 balance = IERC20(token).balanceOf(requestRedeemer); - return balance >= requiredLiquidity.convertFromBase18(tokenDecimals); + (uint256 amountMTokenInUsd, uint256 mTokenRate) = _convertMTokenToUsd( + amountMTokenIn, + overrideMTokenRate + ); + (uint256 amountTokenOut, uint256 tokenOutRate) = _convertUsdToToken( + amountMTokenInUsd, + tokenOut, + overrideTokenOutRate + ); + return (amountTokenOut, mTokenRate, tokenOutRate); + } + + /** + * @dev reverts if the caller is not the contract itself + */ + function _requireSelfCall() private view { + require(msg.sender == address(this), NotSelfCall()); } /** - * @dev gets and validates mToken rate - * @return mTokenRate mToken rate + * @dev calculates holdback part rate from avg rate + * @param request request + * @param avgMTokenRate avg mToken rate + * @return holdback part rate */ - function _getMTokenRate() private view returns (uint256 mTokenRate) { - mTokenRate = _getTokenRate(address(mTokenDataFeed), false); - require(mTokenRate > 0, "RV: rate zero"); + function _calculateHoldbackPartRateFromAvg( + Request memory request, + uint256 avgMTokenRate + ) internal pure returns (uint256) { + if (request.amountMTokenInstant == 0) { + return 0; + } + + uint256 targetTotalValue = ((request.amountMToken + + request.amountMTokenInstant) * avgMTokenRate) / (10**18); + uint256 instantPartValue = ((request.amountMTokenInstant * + request.mTokenRate) / (10**18)); + + if (targetTotalValue <= instantPartValue) { + return 0; + } + + uint256 holdbackPartValue = targetTotalValue - instantPartValue; + + return (holdbackPartValue * (10**18)) / request.amountMToken; } } diff --git a/contracts/RedemptionVaultWithAave.sol b/contracts/RedemptionVaultWithAave.sol index 2ac74b85..bb56ed88 100644 --- a/contracts/RedemptionVaultWithAave.sol +++ b/contracts/RedemptionVaultWithAave.sol @@ -1,12 +1,12 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import "./RedemptionVault.sol"; +import {RedemptionVault} from "./RedemptionVault.sol"; -import "./interfaces/aave/IAaveV3Pool.sol"; -import "./libraries/DecimalsCorrectionLibrary.sol"; +import {IAaveV3Pool} from "./interfaces/aave/IAaveV3Pool.sol"; +import {DecimalsCorrectionLibrary} from "./libraries/DecimalsCorrectionLibrary.sol"; /** * @title RedemptionVaultWithAave @@ -30,22 +30,49 @@ contract RedemptionVaultWithAave is RedemptionVault { /** * @notice Emitted when an Aave V3 Pool is configured for a payment token - * @param caller address of the caller * @param token payment token address * @param pool Aave V3 Pool address */ - event SetAavePool( - address indexed caller, - address indexed token, - address indexed pool - ); + event SetAavePool(address indexed token, address indexed pool); /** * @notice Emitted when an Aave V3 Pool is removed for a payment token - * @param caller address of the caller * @param token payment token address */ - event RemoveAavePool(address indexed caller, address indexed token); + event RemoveAavePool(address indexed token); + + /** + * @notice when token is not in aave pool + * @param aavePool Aave V3 Pool address + * @param token token address + */ + error TokenNotInPool(address aavePool, address token); + + /** + * @notice when pool is not set + * @param token token address + */ + error PoolNotSet(address token); + + /** + * @notice when insufficient withdrawn amount + * @param withdrawnAmount withdrawn amount + * @param toWithdraw amount to withdraw + */ + error InsufficientWithdrawnAmount( + uint256 withdrawnAmount, + uint256 toWithdraw + ); + + /** + * @notice Passes role identifiers to the base RedemptionVault constructor + * @param _contractAdminRole contract admin role identifier + * @param _greenlistedRole greenlisted role identifier + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(bytes32 _contractAdminRole, bytes32 _greenlistedRole) + RedemptionVault(_contractAdminRole, _greenlistedRole) + {} /** * @notice Sets the Aave V3 Pool for a specific payment token @@ -54,148 +81,90 @@ contract RedemptionVaultWithAave is RedemptionVault { */ function setAavePool(address _token, address _aavePool) external - onlyVaultAdmin + onlyContractAdmin { _validateAddress(_token, true); _validateAddress(_aavePool, true); require( IAaveV3Pool(_aavePool).getReserveAToken(_token) != address(0), - "RVA: token not in pool" + TokenNotInPool(_aavePool, _token) ); aavePools[_token] = IAaveV3Pool(_aavePool); - emit SetAavePool(msg.sender, _token, _aavePool); + emit SetAavePool(_token, _aavePool); } /** * @notice Removes the Aave V3 Pool for a specific payment token * @param _token payment token address */ - function removeAavePool(address _token) external onlyVaultAdmin { - require(address(aavePools[_token]) != address(0), "RVA: pool not set"); + function removeAavePool(address _token) external onlyContractAdmin { + require(address(aavePools[_token]) != address(0), PoolNotSet(_token)); delete aavePools[_token]; - emit RemoveAavePool(msg.sender, _token); + emit RemoveAavePool(_token); } /** - * @dev Redeem mToken to the selected payment token if daily limit and allowance are not exceeded. - * If the contract doesn't have enough payment token, the Aave V3 withdrawal flow will be - * triggered to withdraw the missing amount from the Aave Pool. - * Burns mToken from the user. - * Transfers fee in mToken to feeReceiver. - * Transfers tokenOut to user. - * @param tokenOut token out address - * @param amountMTokenIn amount of mToken to redeem - * @param minReceiveAmount minimum expected amount of tokenOut to receive (decimals 18) - * @param recipient address that will receive the tokenOut + * @notice Check if contract has enough tokenOut balance for redeem; + * if not, withdraw the missing amount from the Aave V3 Pool + * @dev The Aave Pool burns the vault's aTokens and transfers the underlying + * asset directly to this contract. No approval is needed because the Pool + * burns aTokens from msg.sender (this contract) internally. + * @param tokenOut tokenOut address + * @param missingAmountBase18 amount of tokenOut needed in base 18 + * @param tokenOutDecimals decimals of tokenOut */ - function _redeemInstant( + function _obtainVaultLiquidity( address tokenOut, - uint256 amountMTokenIn, - uint256 minReceiveAmount, - address recipient + uint256 missingAmountBase18, + uint256, /* tokenOutRate */ + uint256, /* currentTokenOutBalanceBase18 */ + uint256 tokenOutDecimals ) internal + virtual override returns ( - CalcAndValidateRedeemResult memory calcResult, - uint256 amountTokenOutWithoutFee + uint256 /* obtainedLiquidityBase18 */ ) { - address user = msg.sender; - - calcResult = _calcAndValidateRedeem( - user, - tokenOut, - amountMTokenIn, - true, - false - ); - - _requireAndUpdateLimit(amountMTokenIn); - - uint256 tokenDecimals = _tokenDecimals(tokenOut); + IAaveV3Pool pool = aavePools[tokenOut]; - uint256 amountMTokenInCopy = amountMTokenIn; - address tokenOutCopy = tokenOut; - uint256 minReceiveAmountCopy = minReceiveAmount; + // if we dont have a pool for the token, we can't withdraw, so do nothing + if (address(pool) == address(0)) { + return 0; + } - (uint256 amountMTokenInUsd, uint256 mTokenRate) = _convertMTokenToUsd( - amountMTokenInCopy + uint256 missingAmount = missingAmountBase18.convertFromBase18( + tokenOutDecimals ); - (uint256 amountTokenOut, uint256 tokenOutRate) = _convertUsdToToken( - amountMTokenInUsd, - tokenOutCopy - ); - - _requireAndUpdateAllowance(tokenOutCopy, amountTokenOut); - - mToken.burn(user, calcResult.amountMTokenWithoutFee); - if (calcResult.feeAmount > 0) - _tokenTransferFromUser( - address(mToken), - feeReceiver, - calcResult.feeAmount, - 18 - ); - uint256 amountTokenOutWithoutFeeFrom18 = ((calcResult - .amountMTokenWithoutFee * mTokenRate) / tokenOutRate) - .convertFromBase18(tokenDecimals); + address aToken = pool.getReserveAToken(tokenOut); - amountTokenOutWithoutFee = amountTokenOutWithoutFeeFrom18 - .convertToBase18(tokenDecimals); + // if we cant find the aToken, we can't withdraw, so do nothing + if (aToken == address(0)) { + return 0; + } - require( - amountTokenOutWithoutFee >= minReceiveAmountCopy, - "RVA: minReceiveAmount > actual" - ); + uint256 aTokenBalance = IERC20(aToken).balanceOf(address(this)); - _checkAndRedeemAave(tokenOutCopy, amountTokenOutWithoutFeeFrom18); + uint256 toWithdraw = aTokenBalance >= missingAmount + ? missingAmount + : aTokenBalance; - _tokenTransferToUser( - tokenOutCopy, - recipient, - amountTokenOutWithoutFee, - tokenDecimals - ); - } + if (toWithdraw == 0) { + return 0; + } - /** - * @notice Check if contract has enough tokenOut balance for redeem; - * if not, withdraw the missing amount from the Aave V3 Pool - * @dev The Aave Pool burns the vault's aTokens and transfers the underlying - * asset directly to this contract. No approval is needed because the Pool - * burns aTokens from msg.sender (this contract) internally. - * @param tokenOut tokenOut address - * @param amountTokenOut amount of tokenOut needed - */ - function _checkAndRedeemAave(address tokenOut, uint256 amountTokenOut) - internal - { - uint256 contractBalanceTokenOut = IERC20(tokenOut).balanceOf( + uint256 withdrawnAmount = pool.withdraw( + tokenOut, + toWithdraw, address(this) ); - if (contractBalanceTokenOut >= amountTokenOut) return; - - IAaveV3Pool pool = aavePools[tokenOut]; - require(address(pool) != address(0), "RVA: no pool for token"); - - uint256 missingAmount = amountTokenOut - contractBalanceTokenOut; - - address aToken = pool.getReserveAToken(tokenOut); - require(aToken != address(0), "RVA: token not in Aave pool"); - - uint256 aTokenBalance = IERC20(aToken).balanceOf(address(this)); require( - aTokenBalance >= missingAmount, - "RVA: insufficient aToken balance" + withdrawnAmount >= toWithdraw, + InsufficientWithdrawnAmount(withdrawnAmount, toWithdraw) ); - uint256 withdrawnAmount = pool.withdraw( - tokenOut, - missingAmount, - address(this) - ); - require(withdrawnAmount >= missingAmount, "RVA: withdrawn < needed"); + return withdrawnAmount.convertToBase18(tokenOutDecimals); } } diff --git a/contracts/RedemptionVaultWithBUIDL.sol b/contracts/RedemptionVaultWithBUIDL.sol deleted file mode 100644 index 488ce356..00000000 --- a/contracts/RedemptionVaultWithBUIDL.sol +++ /dev/null @@ -1,228 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; - -import "./RedemptionVault.sol"; - -import "./interfaces/buidl/IRedemption.sol"; -import "./libraries/DecimalsCorrectionLibrary.sol"; - -/** - * @title RedemptionVault - * @notice Smart contract that handles mToken redemptions - * @author RedDuck Software - */ -contract RedemptionVaultWIthBUIDL is RedemptionVault { - using DecimalsCorrectionLibrary for uint256; - using SafeERC20 for IERC20; - - /** - * @notice minimum amount of BUIDL to redeem. Will redeem at least this amount of BUIDL. - */ - uint256 public minBuidlToRedeem; - - uint256 public minBuidlBalance; - - IRedemption public buidlRedemption; - - uint256[50] private __gap; - - /** - * @param minBuidlToRedeem new min amount of BUIDL to redeem - * @param sender address who set new min amount of BUIDL to redeem - */ - event SetMinBuidlToRedeem(uint256 minBuidlToRedeem, address sender); - - /** - * @param minBuidlBalance new `minBuidlBalance` value - * @param sender address who set new `minBuidlBalance` - */ - event SetMinBuidlBalance(uint256 minBuidlBalance, address sender); - - /** - * @notice upgradeable pattern contract`s initializer - * @param _ac address of MidasAccessControll contract - * @param _mTokenInitParams init params for mToken - * @param _receiversInitParams init params for receivers - * @param _instantInitParams init params for instant operations - * @param _sanctionsList address of sanctionsList contract - * @param _variationTolerance percent of prices diviation 1% = 100 - * @param _minAmount basic min amount for operations - * @param _fiatRedemptionInitParams params fiatAdditionalFee, fiatFlatFee, minFiatRedeemAmount - * @param _buidlRedemption BUIDL redemption contract address - * @param _requestRedeemer address is designated for standard redemptions, allowing tokens to be pulled from this address - */ - function initialize( - address _ac, - MTokenInitParams calldata _mTokenInitParams, - ReceiversInitParams calldata _receiversInitParams, - InstantInitParams calldata _instantInitParams, - address _sanctionsList, - uint256 _variationTolerance, - uint256 _minAmount, - FiatRedeptionInitParams calldata _fiatRedemptionInitParams, - address _requestRedeemer, - address _buidlRedemption, - uint256 _minBuidlToRedeem, - uint256 _minBuidlBalance - ) external initializer { - __RedemptionVault_init( - _ac, - _mTokenInitParams, - _receiversInitParams, - _instantInitParams, - _sanctionsList, - _variationTolerance, - _minAmount, - _fiatRedemptionInitParams, - _requestRedeemer - ); - _validateAddress(_buidlRedemption, false); - buidlRedemption = IRedemption(_buidlRedemption); - minBuidlToRedeem = _minBuidlToRedeem; - minBuidlBalance = _minBuidlBalance; - } - - /** - * @notice set min amount of BUIDL to redeem. - * @param _minBuidlToRedeem min amount of BUIDL to redeem - */ - function setMinBuidlToRedeem(uint256 _minBuidlToRedeem) - external - onlyVaultAdmin - { - minBuidlToRedeem = _minBuidlToRedeem; - - emit SetMinBuidlToRedeem(_minBuidlToRedeem, msg.sender); - } - - /** - * @notice set new `minBuidlBalance` value. - * @param _minBuidlBalance new `minBuidlBalance` value - */ - function setMinBuidlBalance(uint256 _minBuidlBalance) - external - onlyVaultAdmin - { - minBuidlBalance = _minBuidlBalance; - - emit SetMinBuidlBalance(_minBuidlBalance, msg.sender); - } - - /** - * @dev redeem mToken to USDC if daily limit and allowance not exceeded - * If contract don't have enough USDC, BUIDL redemption flow will be triggered - * Burns mToken from the user. - * Transfers fee in mToken to feeReceiver - * Transfers tokenOut to user. - * @param tokenOut token out address, always ignore - * @param amountMTokenIn amount of mToken to redeem - * @param minReceiveAmount minimum expected amount of tokenOut to receive (decimals 18) - */ - function _redeemInstant( - address tokenOut, - uint256 amountMTokenIn, - uint256 minReceiveAmount, - address recipient - ) - internal - override - returns ( - CalcAndValidateRedeemResult memory calcResult, - uint256 amountTokenOutWithoutFee - ) - { - address user = msg.sender; - - require(buidlRedemption.liquidity() == tokenOut, "RVB: invalid token"); - - calcResult = _calcAndValidateRedeem( - user, - tokenOut, - amountMTokenIn, - true, - false - ); - - _requireAndUpdateLimit(amountMTokenIn); - - uint256 tokenDecimals = _tokenDecimals(tokenOut); - - uint256 amountMTokenInCopy = amountMTokenIn; - address tokenOutCopy = tokenOut; - uint256 minReceiveAmountCopy = minReceiveAmount; - - (uint256 amountMTokenInUsd, uint256 mTokenRate) = _convertMTokenToUsd( - amountMTokenInCopy - ); - (uint256 amountTokenOut, uint256 tokenOutRate) = _convertUsdToToken( - amountMTokenInUsd, - tokenOutCopy - ); - - _requireAndUpdateAllowance(tokenOutCopy, amountTokenOut); - - mToken.burn(user, calcResult.amountMTokenWithoutFee); - if (calcResult.feeAmount > 0) - _tokenTransferFromUser( - address(mToken), - feeReceiver, - calcResult.feeAmount, - 18 - ); - - amountTokenOutWithoutFee = - (calcResult.amountMTokenWithoutFee * mTokenRate) / - tokenOutRate; - - require( - amountTokenOutWithoutFee >= minReceiveAmountCopy, - "RVB: minReceiveAmount > actual" - ); - - uint256 amountTokenOutWithoutFeeFrom18 = amountTokenOutWithoutFee - .convertFromBase18(tokenDecimals); - - _checkAndRedeemBUIDL(tokenOutCopy, amountTokenOutWithoutFeeFrom18); - - _tokenTransferToUser( - tokenOutCopy, - recipient, - amountTokenOutWithoutFeeFrom18.convertToBase18(tokenDecimals), - tokenDecimals - ); - } - - /** - * @notice Check if contract have enough USDC balance for redeem - * if don't have trigger BUIDL redemption flow - * @param tokenOut tokenOut address - * @param amountTokenOut amount of tokenOut - */ - function _checkAndRedeemBUIDL(address tokenOut, uint256 amountTokenOut) - internal - { - uint256 contractBalanceTokenOut = IERC20(tokenOut).balanceOf( - address(this) - ); - if (contractBalanceTokenOut >= amountTokenOut) return; - - uint256 buidlToRedeem = amountTokenOut - contractBalanceTokenOut; - if (buidlToRedeem < minBuidlToRedeem) { - buidlToRedeem = minBuidlToRedeem; - } - IERC20 buidl = IERC20(buidlRedemption.asset()); - uint256 buidlBalance = buidl.balanceOf(address(this)); - require(buidlBalance >= buidlToRedeem, "RVB: buidlToRedeem > balance"); - if ( - buidlBalance - buidlToRedeem <= minBuidlToRedeem || - buidlBalance - buidlToRedeem <= minBuidlBalance - ) { - buidlToRedeem = buidlBalance; - } - buidl.safeIncreaseAllowance(address(buidlRedemption), buidlToRedeem); - buidlRedemption.redeem(buidlToRedeem); - } -} diff --git a/contracts/RedemptionVaultWithMToken.sol b/contracts/RedemptionVaultWithMToken.sol index 7fa9024d..14c884cf 100644 --- a/contracts/RedemptionVaultWithMToken.sol +++ b/contracts/RedemptionVaultWithMToken.sol @@ -1,14 +1,17 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import "./RedemptionVault.sol"; -import "./interfaces/IRedemptionVault.sol"; -import "./libraries/DecimalsCorrectionLibrary.sol"; +import {RedemptionVault} from "./RedemptionVault.sol"; +import {ManageableVault} from "./abstract/ManageableVault.sol"; +import {DecimalsCorrectionLibrary} from "./libraries/DecimalsCorrectionLibrary.sol"; +import {IRedemptionVault, RedemptionVaultInitParams} from "./interfaces/IRedemptionVault.sol"; +import {CommonVaultInitParams} from "./interfaces/IManageableVault.sol"; +import {RedemptionSwapperHelpersLibrary} from "./libraries/RedemptionSwapperHelpersLibrary.sol"; /** * @title RedemptionVaultWithMToken @@ -20,24 +23,11 @@ contract RedemptionVaultWithMToken is RedemptionVault { using DecimalsCorrectionLibrary for uint256; using SafeERC20 for IERC20; - /** - * @dev Storage gap preserved from RedemptionVaultWithSwapper layout - */ - uint256[50] private ___gap; - /** * @notice mToken RedemptionVault used for fallback redemptions - * @custom:oz-renamed-from mTbillRedemptionVault */ IRedemptionVault public redemptionVault; - /** - * @dev Deprecated storage slot preserved for storage layout compatibility. Must not be removed - * @custom:oz-renamed-from liquidityProvider - */ - // solhint-disable-next-line var-name-mixedcase - address public liquidityProvider_deprecated; - /** * @dev leaving a storage gap for futures updates */ @@ -45,47 +35,32 @@ contract RedemptionVaultWithMToken is RedemptionVault { /** * @notice Emitted when the redemption vault address is updated - * @param caller address of the caller * @param newVault new redemption vault address */ - event SetRedemptionVault(address indexed caller, address indexed newVault); + event SetRedemptionVault(address indexed newVault); + + /** + * @notice Passes role identifiers to the base RedemptionVault constructor + * @param _contractAdminRole contract admin role identifier + * @param _greenlistedRole greenlisted role identifier + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(bytes32 _contractAdminRole, bytes32 _greenlistedRole) + RedemptionVault(_contractAdminRole, _greenlistedRole) + {} /** * @notice upgradeable pattern contract`s initializer - * @param _ac address of MidasAccessControll contract - * @param _mTokenInitParams init params for mToken - * @param _receiversInitParams init params for receivers - * @param _instantInitParams init params for instant operations - * @param _sanctionsList address of sanctionsList contract - * @param _variationTolerance percent of prices diviation 1% = 100 - * @param _minAmount basic min amount for operations - * @param _fiatRedemptionInitParams params fiatAdditionalFee, fiatFlatFee, minFiatRedeemAmount - * @param _requestRedeemer address is designated for standard redemptions, allowing tokens to be pulled from this address + * @param _commonVaultInitParams init params for common vault + * @param _redemptionInitParams init params for redemption vault state values * @param _redemptionVault address of the mTokenA RedemptionVault */ function initialize( - address _ac, - MTokenInitParams calldata _mTokenInitParams, - ReceiversInitParams calldata _receiversInitParams, - InstantInitParams calldata _instantInitParams, - address _sanctionsList, - uint256 _variationTolerance, - uint256 _minAmount, - FiatRedeptionInitParams calldata _fiatRedemptionInitParams, - address _requestRedeemer, + CommonVaultInitParams calldata _commonVaultInitParams, + RedemptionVaultInitParams calldata _redemptionInitParams, address _redemptionVault - ) external initializer { - __RedemptionVault_init( - _ac, - _mTokenInitParams, - _receiversInitParams, - _instantInitParams, - _sanctionsList, - _variationTolerance, - _minAmount, - _fiatRedemptionInitParams, - _requestRedeemer - ); + ) external { + initialize(_commonVaultInitParams, _redemptionInitParams); _validateAddress(_redemptionVault, true); redemptionVault = IRedemptionVault(_redemptionVault); } @@ -96,142 +71,52 @@ contract RedemptionVaultWithMToken is RedemptionVault { */ function setRedemptionVault(address _redemptionVault) external - onlyVaultAdmin + onlyContractAdmin { require( _redemptionVault != address(redemptionVault), - "RVMT: already set" + SameAddressValue(_redemptionVault) ); _validateAddress(_redemptionVault, true); redemptionVault = IRedemptionVault(_redemptionVault); - emit SetRedemptionVault(msg.sender, _redemptionVault); + emit SetRedemptionVault(_redemptionVault); } /** - * @dev Redeem mToken to the selected payment token if daily limit and allowance are not exceeded. - * If the contract doesn't have enough payment token, the mToken RedemptionVault flow - * will be triggered to redeem the missing amount. - * Burns mToken from the user. - * Transfers fee in mToken to feeReceiver. - * Transfers tokenOut to user. - * @param tokenOut token out address - * @param amountMTokenIn amount of mToken to redeem - * @param minReceiveAmount minimum expected amount of tokenOut to receive (decimals 18) - * @param recipient address that will receive the tokenOut + * @notice Check if contract has enough tokenOut balance for redeem; + * if not, redeem the missing amount via mToken RedemptionVault + * @dev The other vault burns this contract's mToken and transfers the + * underlying asset to this contract + * @param tokenOut tokenOut address + * @param missingAmountBase18 amount of tokenOut needed in base 18 + * @param tokenOutRate tokenOut rate */ - function _redeemInstant( + function _obtainVaultLiquidity( address tokenOut, - uint256 amountMTokenIn, - uint256 minReceiveAmount, - address recipient + uint256 missingAmountBase18, + uint256 tokenOutRate, + uint256, /* currentTokenOutBalanceBase18 */ + uint256 tokenOutDecimals ) internal + virtual override returns ( - CalcAndValidateRedeemResult memory calcResult, - uint256 amountTokenOutWithoutFee + uint256 /* obtainedLiquidityBase18 */ ) { - address user = msg.sender; - - calcResult = _calcAndValidateRedeem( - user, - tokenOut, - amountMTokenIn, - true, - false - ); - - _requireAndUpdateLimit(amountMTokenIn); - - uint256 tokenDecimals = _tokenDecimals(tokenOut); - - uint256 amountMTokenInCopy = amountMTokenIn; - address tokenOutCopy = tokenOut; - uint256 minReceiveAmountCopy = minReceiveAmount; - - (uint256 amountMTokenInUsd, uint256 mTokenRate) = _convertMTokenToUsd( - amountMTokenInCopy - ); - (uint256 amountTokenOut, uint256 tokenOutRate) = _convertUsdToToken( - amountMTokenInUsd, - tokenOutCopy - ); - - _requireAndUpdateAllowance(tokenOutCopy, amountTokenOut); - - mToken.burn(user, calcResult.amountMTokenWithoutFee); - if (calcResult.feeAmount > 0) - _tokenTransferFromUser( - address(mToken), - feeReceiver, - calcResult.feeAmount, - 18 - ); - - uint256 amountTokenOutWithoutFeeFrom18 = ((calcResult - .amountMTokenWithoutFee * mTokenRate) / tokenOutRate) - .convertFromBase18(tokenDecimals); - - amountTokenOutWithoutFee = amountTokenOutWithoutFeeFrom18 - .convertToBase18(tokenDecimals); - - require( - amountTokenOutWithoutFee >= minReceiveAmountCopy, - "RVMT: minReceiveAmount > actual" - ); - - _checkAndRedeemMToken( - tokenOutCopy, - amountTokenOutWithoutFeeFrom18, - tokenOutRate - ); - - _tokenTransferToUser( - tokenOutCopy, - recipient, - amountTokenOutWithoutFee, - tokenDecimals - ); - } - - /** - * @notice Check if contract has enough tokenOut balance for redeem; - * if not, redeem the missing amount via mToken RedemptionVault - * @dev The other vault burns this contract's mToken and transfers the - * underlying asset to this contract - * @param tokenOut tokenOut address - * @param amountTokenOut amount of tokenOut needed (native decimals) - * @param tokenOutRate tokenOut price rate (decimals 18) - */ - function _checkAndRedeemMToken( - address tokenOut, - uint256 amountTokenOut, - uint256 tokenOutRate - ) internal { - uint256 tokenDecimals = _tokenDecimals(tokenOut); - uint256 amountTokenOutBase18 = amountTokenOut.convertToBase18( - tokenDecimals - ); - uint256 contractBalanceBase18 = IERC20(tokenOut) - .balanceOf(address(this)) - .convertToBase18(tokenDecimals); - if (contractBalanceBase18 >= amountTokenOutBase18) return; - - require( - ManageableVault(address(redemptionVault)).waivedFeeRestriction( + IRedemptionVault _redemptionVault = redemptionVault; + + ( + uint256 mTokenARate, + IERC20 mTokenA, + uint256 mTokenABalance + ) = RedemptionSwapperHelpersLibrary.getSwapperDetails( + _redemptionVault, address(this) - ), - "RVMT: fees not waived on target" - ); - - uint256 missingAmountBase18 = amountTokenOutBase18 - - contractBalanceBase18; - uint256 mTokenARate = redemptionVault - .mTokenDataFeed() - .getDataInBase18(); + ); // Ceil so the inner vault's floored output is still >= missingAmountBase18. uint256 mTokenAAmount = Math.mulDiv( @@ -241,22 +126,26 @@ contract RedemptionVaultWithMToken is RedemptionVault { Math.Rounding.Up ); - address mTokenA = address(redemptionVault.mToken()); - - require( - IERC20(mTokenA).balanceOf(address(this)) >= mTokenAAmount, - "RVMT: balance < needed" - ); - - IERC20(mTokenA).safeIncreaseAllowance( - address(redemptionVault), - mTokenAAmount - ); - - redemptionVault.redeemInstant( - tokenOut, - mTokenAAmount, - missingAmountBase18 - ); + mTokenAAmount = mTokenABalance >= mTokenAAmount + ? mTokenAAmount + : mTokenABalance; + + if (!_redemptionVault.waivedFeeRestriction(address(this))) { + return 0; + } + + if (mTokenAAmount == 0) { + return 0; + } + + return + RedemptionSwapperHelpersLibrary.redeemInstantSwapper( + _redemptionVault, + mTokenA, + address(this), + tokenOut, + mTokenAAmount, + tokenOutDecimals + ); } } diff --git a/contracts/RedemptionVaultWithMorpho.sol b/contracts/RedemptionVaultWithMorpho.sol index bb4db3d8..8abe33f5 100644 --- a/contracts/RedemptionVaultWithMorpho.sol +++ b/contracts/RedemptionVaultWithMorpho.sol @@ -1,12 +1,12 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import "./RedemptionVault.sol"; +import {RedemptionVault} from "./RedemptionVault.sol"; import {IMorphoVault} from "./interfaces/morpho/IMorphoVault.sol"; -import "./libraries/DecimalsCorrectionLibrary.sol"; +import {DecimalsCorrectionLibrary} from "./libraries/DecimalsCorrectionLibrary.sol"; /** * @title RedemptionVaultWithMorpho @@ -31,22 +31,39 @@ contract RedemptionVaultWithMorpho is RedemptionVault { /** * @notice Emitted when a Morpho Vault is configured for a payment token - * @param caller address of the caller * @param token payment token address * @param vault Morpho Vault address */ - event SetMorphoVault( - address indexed caller, - address indexed token, - address indexed vault - ); + event SetMorphoVault(address indexed token, address indexed vault); /** * @notice Emitted when a Morpho Vault is removed for a payment token - * @param caller address of the caller * @param token payment token address */ - event RemoveMorphoVault(address indexed caller, address indexed token); + event RemoveMorphoVault(address indexed token); + + /** + * @notice when asset mismatch + * @param morphoVault Morpho Vault address + * @param token token address + */ + error AssetMismatch(address morphoVault, address token); + + /** + * @notice when vault is not set + * @param token token address + */ + error VaultNotSet(address token); + + /** + * @notice Passes role identifiers to the base RedemptionVault constructor + * @param _contractAdminRole contract admin role identifier + * @param _greenlistedRole greenlisted role identifier + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(bytes32 _contractAdminRole, bytes32 _greenlistedRole) + RedemptionVault(_contractAdminRole, _greenlistedRole) + {} /** * @notice Sets the Morpho Vault for a specific payment token @@ -55,113 +72,29 @@ contract RedemptionVaultWithMorpho is RedemptionVault { */ function setMorphoVault(address _token, address _morphoVault) external - onlyVaultAdmin + onlyContractAdmin { _validateAddress(_token, true); _validateAddress(_morphoVault, true); require( IMorphoVault(_morphoVault).asset() == _token, - "RVM: asset mismatch" + AssetMismatch(_morphoVault, _token) ); morphoVaults[_token] = IMorphoVault(_morphoVault); - emit SetMorphoVault(msg.sender, _token, _morphoVault); + emit SetMorphoVault(_token, _morphoVault); } /** * @notice Removes the Morpho Vault for a specific payment token * @param _token payment token address */ - function removeMorphoVault(address _token) external onlyVaultAdmin { + function removeMorphoVault(address _token) external onlyContractAdmin { require( address(morphoVaults[_token]) != address(0), - "RVM: vault not set" + VaultNotSet(_token) ); delete morphoVaults[_token]; - emit RemoveMorphoVault(msg.sender, _token); - } - - /** - * @dev Redeem mToken to the selected payment token if daily limit and allowance are not exceeded. - * If the contract doesn't have enough payment token, the Morpho Vault withdrawal flow will be - * triggered to withdraw the missing amount from the Morpho Vault. - * Burns mToken from the user. - * Transfers fee in mToken to feeReceiver. - * Transfers tokenOut to user. - * @param tokenOut token out address - * @param amountMTokenIn amount of mToken to redeem - * @param minReceiveAmount minimum expected amount of tokenOut to receive (decimals 18) - * @param recipient address that will receive the tokenOut - */ - function _redeemInstant( - address tokenOut, - uint256 amountMTokenIn, - uint256 minReceiveAmount, - address recipient - ) - internal - override - returns ( - CalcAndValidateRedeemResult memory calcResult, - uint256 amountTokenOutWithoutFee - ) - { - address user = msg.sender; - - calcResult = _calcAndValidateRedeem( - user, - tokenOut, - amountMTokenIn, - true, - false - ); - - _requireAndUpdateLimit(amountMTokenIn); - - uint256 tokenDecimals = _tokenDecimals(tokenOut); - - uint256 amountMTokenInCopy = amountMTokenIn; - address tokenOutCopy = tokenOut; - uint256 minReceiveAmountCopy = minReceiveAmount; - - (uint256 amountMTokenInUsd, uint256 mTokenRate) = _convertMTokenToUsd( - amountMTokenInCopy - ); - (uint256 amountTokenOut, uint256 tokenOutRate) = _convertUsdToToken( - amountMTokenInUsd, - tokenOutCopy - ); - - _requireAndUpdateAllowance(tokenOutCopy, amountTokenOut); - - mToken.burn(user, calcResult.amountMTokenWithoutFee); - if (calcResult.feeAmount > 0) - _tokenTransferFromUser( - address(mToken), - feeReceiver, - calcResult.feeAmount, - 18 - ); - - uint256 amountTokenOutWithoutFeeFrom18 = ((calcResult - .amountMTokenWithoutFee * mTokenRate) / tokenOutRate) - .convertFromBase18(tokenDecimals); - - amountTokenOutWithoutFee = amountTokenOutWithoutFeeFrom18 - .convertToBase18(tokenDecimals); - - require( - amountTokenOutWithoutFee >= minReceiveAmountCopy, - "RVM: minReceiveAmount > actual" - ); - - _checkAndRedeemMorpho(tokenOutCopy, amountTokenOutWithoutFeeFrom18); - - _tokenTransferToUser( - tokenOutCopy, - recipient, - amountTokenOutWithoutFee, - tokenDecimals - ); + emit RemoveMorphoVault(_token); } /** @@ -171,27 +104,46 @@ contract RedemptionVaultWithMorpho is RedemptionVault { * asset directly to this contract. No approval is needed because the vault * burns shares from msg.sender (this contract) when msg.sender == owner. * @param tokenOut tokenOut address - * @param amountTokenOut amount of tokenOut needed + * @param missingAmountBase18 amount of tokenOut needed in base 18 + * @param tokenOutDecimals decimals of tokenOut */ - function _checkAndRedeemMorpho(address tokenOut, uint256 amountTokenOut) + function _obtainVaultLiquidity( + address tokenOut, + uint256 missingAmountBase18, + uint256, /* tokenOutRate */ + uint256, /* currentTokenOutBalanceBase18 */ + uint256 tokenOutDecimals + ) internal + virtual + override + returns ( + uint256 /* obtainedLiquidityBase18 */ + ) { - uint256 contractBalanceTokenOut = IERC20(tokenOut).balanceOf( - address(this) - ); - if (contractBalanceTokenOut >= amountTokenOut) return; - IMorphoVault vault = morphoVaults[tokenOut]; - require(address(vault) != address(0), "RVM: no vault for token"); - uint256 missingAmount = amountTokenOut - contractBalanceTokenOut; + if (address(vault) == address(0)) { + return 0; + } - uint256 sharesNeeded = vault.previewWithdraw(missingAmount); - require( - vault.balanceOf(address(this)) >= sharesNeeded, - "RVM: insufficient shares" + uint256 missingAmount = missingAmountBase18.convertFromBase18( + tokenOutDecimals ); - vault.withdraw(missingAmount, address(this), address(this)); + uint256 sharesNeeded = vault.previewWithdraw(missingAmount); + uint256 vaultSharesBalance = vault.balanceOf(address(this)); + uint256 toRedeemShares = vaultSharesBalance >= sharesNeeded + ? sharesNeeded + : vaultSharesBalance; + + if (toRedeemShares == 0) { + return 0; + } + + return + vault + .redeem(toRedeemShares, address(this), address(this)) + .convertToBase18(tokenOutDecimals); } } diff --git a/contracts/RedemptionVaultWithSwapper.sol b/contracts/RedemptionVaultWithSwapper.sol deleted file mode 100644 index b6de1bc8..00000000 --- a/contracts/RedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,265 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; - -import "./RedemptionVault.sol"; -import "./interfaces/IRedemptionVault.sol"; -import "./interfaces/IRedemptionVaultWithSwapper.sol"; -import "./libraries/DecimalsCorrectionLibrary.sol"; - -/** - * @title RedemptionVaultWithSwapper - * @notice Smart contract that handles mToken redemption. - * In case of insufficient liquidity it uses a RV from a different - * Midas product to fulfill instant redemption. - * @dev mToken1 - is a main mToken of this vault - * mToken2 - is a token of a second vault that is triggered when - * current vault don`t have enough liquidity - * @author RedDuck Software - */ -contract RedemptionVaultWithSwapper is - IRedemptionVaultWithSwapper, - RedemptionVault -{ - using DecimalsCorrectionLibrary for uint256; - using SafeERC20 for IERC20; - - /** - * @dev added second gap here to match the storage layout - * from the previous contracts inheritance tree - */ - uint256[50] private ___gap; - - /** - * @notice mToken1 redemption vault - * @dev The naming was not altered to maintain - * compatibility with the currently deployed contracts. - */ - IRedemptionVault public mTbillRedemptionVault; - - address public liquidityProvider; - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @notice upgradeable pattern contract`s initializer - * @param _ac address of MidasAccessControll contract - * @param _mTokenInitParams init params for mToken1 - * @param _receiversInitParams init params for receivers - * @param _instantInitParams init params for instant operations - * @param _sanctionsList address of sanctionsList contract - * @param _variationTolerance percent of prices diviation 1% = 100 - * @param _minAmount basic min amount for operations - * @param _fiatRedemptionInitParams params fiatAdditionalFee, fiatFlatFee, minFiatRedeemAmount - * @param _requestRedeemer address is designated for standard redemptions, allowing tokens to be pulled from this address - * @param _mTbillRedemptionVault mToken2 redemptionVault address - * @param _liquidityProvider liquidity provider for pull mToken2 - */ - function initialize( - address _ac, - MTokenInitParams calldata _mTokenInitParams, - ReceiversInitParams calldata _receiversInitParams, - InstantInitParams calldata _instantInitParams, - address _sanctionsList, - uint256 _variationTolerance, - uint256 _minAmount, - FiatRedeptionInitParams calldata _fiatRedemptionInitParams, - address _requestRedeemer, - address _mTbillRedemptionVault, - address _liquidityProvider - ) external initializer { - __RedemptionVault_init( - _ac, - _mTokenInitParams, - _receiversInitParams, - _instantInitParams, - _sanctionsList, - _variationTolerance, - _minAmount, - _fiatRedemptionInitParams, - _requestRedeemer - ); - _validateAddress(_mTbillRedemptionVault, true); - _validateAddress(_liquidityProvider, false); - - mTbillRedemptionVault = IRedemptionVault(_mTbillRedemptionVault); - liquidityProvider = _liquidityProvider; - } - - /** - * @dev redeem mToken1 to tokenOut if daily limit and allowance not exceeded - * If contract don't have enough tokenOut, mToken1 will swap to mToken2 and redeem on mToken2 vault - * Burns mToken1 from the user, if swap need mToken1 just tranfers to contract. - * Transfers fee in mToken1 to feeReceiver - * Transfers tokenOut to user. - * @param tokenOut token out address - * @param amountMTokenIn amount of mToken1 to redeem - * @param minReceiveAmount minimum expected amount of tokenOut to receive (decimals 18) - */ - function _redeemInstant( - address tokenOut, - uint256 amountMTokenIn, - uint256 minReceiveAmount, - address recipient - ) - internal - override - returns ( - CalcAndValidateRedeemResult memory calcResult, - uint256 amountTokenOutWithoutFee - ) - { - address user = msg.sender; - - calcResult = _calcAndValidateRedeem( - user, - tokenOut, - amountMTokenIn, - true, - false - ); - - uint256 tokenDecimals = _tokenDecimals(tokenOut); - - uint256 amountMTokenInCopy = amountMTokenIn; - address tokenOutCopy = tokenOut; - uint256 minReceiveAmountCopy = minReceiveAmount; - - (uint256 amountMTokenInUsd, uint256 mTokenRate) = _convertMTokenToUsd( - amountMTokenInCopy - ); - (uint256 amountTokenOut, uint256 tokenOutRate) = _convertUsdToToken( - amountMTokenInUsd, - tokenOutCopy - ); - - amountTokenOutWithoutFee = _truncate( - (calcResult.amountMTokenWithoutFee * mTokenRate) / tokenOutRate, - tokenDecimals - ); - - require( - amountTokenOutWithoutFee >= minReceiveAmountCopy, - "RVS: minReceiveAmount > actual" - ); - - if (calcResult.feeAmount > 0) - _tokenTransferFromUser( - address(mToken), - feeReceiver, - calcResult.feeAmount, - 18 - ); - - uint256 contractTokenOutBalance = IERC20(tokenOutCopy).balanceOf( - address(this) - ); - - _requireAndUpdateLimit(amountMTokenInCopy); - _requireAndUpdateAllowance(tokenOutCopy, amountTokenOut); - - if ( - contractTokenOutBalance >= - amountTokenOutWithoutFee.convertFromBase18(tokenDecimals) - ) { - mToken.burn(user, calcResult.amountMTokenWithoutFee); - } else { - uint256 mTbillAmount = _swapMToken1ToMToken2( - calcResult.amountMTokenWithoutFee - ); - - IERC20(mTbillRedemptionVault.mToken()).safeIncreaseAllowance( - address(mTbillRedemptionVault), - mTbillAmount - ); - - mTbillRedemptionVault.redeemInstant( - tokenOutCopy, - mTbillAmount, - minReceiveAmountCopy - ); - - uint256 contractTokenOutBalanceAfterRedeem = IERC20(tokenOutCopy) - .balanceOf(address(this)); - amountTokenOutWithoutFee = (contractTokenOutBalanceAfterRedeem - - contractTokenOutBalance).convertToBase18(tokenDecimals); - } - - _tokenTransferToUser( - tokenOutCopy, - recipient, - amountTokenOutWithoutFee, - tokenDecimals - ); - } - - /** - * @inheritdoc IRedemptionVaultWithSwapper - */ - function setLiquidityProvider(address provider) external onlyVaultAdmin { - require(liquidityProvider != provider, "MRVS: already provider"); - _validateAddress(provider, false); - - liquidityProvider = provider; - - emit SetLiquidityProvider(msg.sender, provider); - } - - /** - * @inheritdoc IRedemptionVaultWithSwapper - */ - function setSwapperVault(address newVault) external onlyVaultAdmin { - require( - newVault != address(mTbillRedemptionVault), - "MRVS: already provider" - ); - _validateAddress(newVault, true); - - mTbillRedemptionVault = IRedemptionVault(newVault); - - emit SetSwapperVault(msg.sender, newVault); - } - - /** - * @notice Transfers mToken1 to liquidity provider - * Transfers mToken2 from liquidity provider to contract - * Returns amount on mToken2 using exchange rates - * @param mToken1Amount mToken1 token amount (decimals 18) - */ - function _swapMToken1ToMToken2(uint256 mToken1Amount) - internal - returns (uint256 mTokenAmount) - { - _tokenTransferFromUser( - address(mToken), - liquidityProvider, - mToken1Amount, - 18 - ); - - uint256 mTbillRate = mTbillRedemptionVault - .mTokenDataFeed() - .getDataInBase18(); - uint256 mTokenRate = mTokenDataFeed.getDataInBase18(); - mTokenAmount = Math.mulDiv( - mToken1Amount, - mTokenRate, - mTbillRate, - Math.Rounding.Up - ); - - _tokenTransferFromTo( - address(mTbillRedemptionVault.mToken()), - liquidityProvider, - address(this), - mTokenAmount, - 18 - ); - } -} diff --git a/contracts/RedemptionVaultWithUSTB.sol b/contracts/RedemptionVaultWithUSTB.sol index 16905126..80cae8ef 100644 --- a/contracts/RedemptionVaultWithUSTB.sol +++ b/contracts/RedemptionVaultWithUSTB.sol @@ -1,13 +1,15 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import "./RedemptionVault.sol"; +import {RedemptionVault} from "./RedemptionVault.sol"; +import {RedemptionVaultInitParams} from "./interfaces/IRedemptionVault.sol"; +import {CommonVaultInitParams} from "./interfaces/IManageableVault.sol"; -import "./interfaces/ustb/IUSTBRedemption.sol"; -import "./libraries/DecimalsCorrectionLibrary.sol"; +import {IUSTBRedemption} from "./interfaces/ustb/IUSTBRedemption.sol"; +import {DecimalsCorrectionLibrary} from "./libraries/DecimalsCorrectionLibrary.sol"; /** * @title RedemptionVaultWithUSTB @@ -39,159 +41,92 @@ contract RedemptionVaultWithUSTB is RedemptionVault { */ uint256[50] private __gap; + /** + * @notice Passes role identifiers to the base RedemptionVault constructor + * @param _contractAdminRole contract admin role identifier + * @param _greenlistedRole greenlisted role identifier + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(bytes32 _contractAdminRole, bytes32 _greenlistedRole) + RedemptionVault(_contractAdminRole, _greenlistedRole) + {} + /** * @notice upgradeable pattern contract`s initializer - * @param _ac address of MidasAccessControll contract - * @param _mTokenInitParams init params for mToken - * @param _receiversInitParams init params for receivers - * @param _instantInitParams init params for instant operations - * @param _sanctionsList address of sanctionsList contract - * @param _variationTolerance percent of prices diviation 1% = 100 - * @param _minAmount basic min amount for operations - * @param _fiatRedemptionInitParams params fiatAdditionalFee, fiatFlatFee, minFiatRedeemAmount - * @param _requestRedeemer address is designated for standard redemptions, allowing tokens to be pulled from this address + * @param _commonVaultInitParams init params for common vault + * @param _redemptionInitParams init params for redemption vault state values * @param _ustbRedemption USTB redemption contract address */ function initialize( - address _ac, - MTokenInitParams calldata _mTokenInitParams, - ReceiversInitParams calldata _receiversInitParams, - InstantInitParams calldata _instantInitParams, - address _sanctionsList, - uint256 _variationTolerance, - uint256 _minAmount, - FiatRedeptionInitParams calldata _fiatRedemptionInitParams, - address _requestRedeemer, + CommonVaultInitParams calldata _commonVaultInitParams, + RedemptionVaultInitParams calldata _redemptionInitParams, address _ustbRedemption - ) external initializer { - __RedemptionVault_init( - _ac, - _mTokenInitParams, - _receiversInitParams, - _instantInitParams, - _sanctionsList, - _variationTolerance, - _minAmount, - _fiatRedemptionInitParams, - _requestRedeemer - ); + ) external { + initialize(_commonVaultInitParams, _redemptionInitParams); _validateAddress(_ustbRedemption, false); ustbRedemption = IUSTBRedemption(_ustbRedemption); } /** - * @dev Redeem mToken to the selected payment token if daily limit and allowance are not exceeded. - * If USDC is the payment token and the contract doesn't have enough USDC, the USTB redemption flow will be triggered for the missing amount. - * Burns mToken from the user. - * Transfers fee in mToken to feeReceiver. - * Transfers tokenOut to user. - * @param tokenOut token out address - * @param amountMTokenIn amount of mToken to redeem - * @param minReceiveAmount minimum expected amount of tokenOut to receive (decimals 18) + * @notice Check if contract has enough USDC balance for redeem + * if not, trigger USTB redemption flow to redeem exactly the missing amount + * @param tokenOut tokenOut address + * @param missingAmountBase18 amount of tokenOut needed in base 18 + * @param currentTokenOutBalanceBase18 current balance of tokenOut in the vault in base 18 + * @param tokenOutDecimals decimals of tokenOut */ - function _redeemInstant( + function _obtainVaultLiquidity( address tokenOut, - uint256 amountMTokenIn, - uint256 minReceiveAmount, - address recipient + uint256 missingAmountBase18, + uint256, /* tokenOutRate */ + uint256 currentTokenOutBalanceBase18, + uint256 tokenOutDecimals ) internal + virtual override returns ( - CalcAndValidateRedeemResult memory calcResult, - uint256 amountTokenOutWithoutFee + uint256 /* obtainedLiquidityBase18 */ ) { - address user = msg.sender; - - calcResult = _calcAndValidateRedeem( - user, - tokenOut, - amountMTokenIn, - true, - false - ); - - _requireAndUpdateLimit(amountMTokenIn); + // If tokenOut is not USDC, do nothing + if (tokenOut != ustbRedemption.USDC()) { + return 0; + } - uint256 tokenDecimals = _tokenDecimals(tokenOut); - - uint256 amountMTokenInCopy = amountMTokenIn; - address tokenOutCopy = tokenOut; - uint256 minReceiveAmountCopy = minReceiveAmount; - - (uint256 amountMTokenInUsd, uint256 mTokenRate) = _convertMTokenToUsd( - amountMTokenInCopy + uint256 missingAmount = missingAmountBase18.convertFromBase18( + tokenOutDecimals ); - (uint256 amountTokenOut, uint256 tokenOutRate) = _convertUsdToToken( - amountMTokenInUsd, - tokenOutCopy - ); - - _requireAndUpdateAllowance(tokenOutCopy, amountTokenOut); - mToken.burn(user, calcResult.amountMTokenWithoutFee); - if (calcResult.feeAmount > 0) - _tokenTransferFromUser( - address(mToken), - feeReceiver, - calcResult.feeAmount, - 18 - ); + IUSTBRedemption _ustbRedemption = ustbRedemption; - uint256 amountTokenOutWithoutFeeFrom18 = ((calcResult - .amountMTokenWithoutFee * mTokenRate) / tokenOutRate) - .convertFromBase18(tokenDecimals); + uint256 fee = _ustbRedemption.calculateFee(missingAmount); - amountTokenOutWithoutFee = amountTokenOutWithoutFeeFrom18 - .convertToBase18(tokenDecimals); + // If fee is not zero, do nothing + if (fee != 0) { + return 0; + } - require( - amountTokenOutWithoutFee >= minReceiveAmountCopy, - "RVU: minReceiveAmount > actual" - ); - - _checkAndRedeemUSTB(tokenOutCopy, amountTokenOutWithoutFeeFrom18); - - _tokenTransferToUser( - tokenOutCopy, - recipient, - amountTokenOutWithoutFee, - tokenDecimals - ); - } - - /** - * @notice Check if contract has enough USDC balance for redeem - * if not, trigger USTB redemption flow to redeem exactly the missing amount - * @param tokenOut tokenOut address - * @param amountTokenOut amount of tokenOut needed - */ - function _checkAndRedeemUSTB(address tokenOut, uint256 amountTokenOut) - internal - { - uint256 contractBalanceTokenOut = IERC20(tokenOut).balanceOf( - address(this) - ); - if (contractBalanceTokenOut >= amountTokenOut) return; - - require(tokenOut == ustbRedemption.USDC(), "RVU: invalid token"); - - uint256 missingAmount = amountTokenOut - contractBalanceTokenOut; - - uint256 fee = ustbRedemption.calculateFee(missingAmount); - require(fee == 0, "RVU: ustb fee not zero"); - - (uint256 ustbToRedeem, ) = ustbRedemption.calculateUstbIn( + (uint256 ustbToRedeem, ) = _ustbRedemption.calculateUstbIn( missingAmount ); - IERC20 ustb = IERC20(ustbRedemption.SUPERSTATE_TOKEN()); + IERC20 ustb = IERC20(_ustbRedemption.SUPERSTATE_TOKEN()); uint256 ustbBalance = ustb.balanceOf(address(this)); - require(ustbBalance >= ustbToRedeem, "RVU: insufficient USTB balance"); + ustbToRedeem = ustbBalance >= ustbToRedeem ? ustbToRedeem : ustbBalance; + + // if nothing to redeem, do nothing + if (ustbToRedeem == 0) { + return 0; + } + + ustb.safeIncreaseAllowance(address(_ustbRedemption), ustbToRedeem); + _ustbRedemption.redeem(ustbToRedeem); - ustb.safeIncreaseAllowance(address(ustbRedemption), ustbToRedeem); - ustbRedemption.redeem(ustbToRedeem); + return + IERC20(tokenOut).balanceOf(address(this)).convertToBase18( + tokenOutDecimals + ) - currentTokenOutBalanceBase18; } } diff --git a/contracts/abstract/ManageableVault.sol b/contracts/abstract/ManageableVault.sol index 0c59123f..68a0798b 100644 --- a/contracts/abstract/ManageableVault.sol +++ b/contracts/abstract/ManageableVault.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import {IERC20MetadataUpgradeable as IERC20Metadata} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; @@ -9,16 +9,21 @@ import {EnumerableSetUpgradeable as EnumerableSet} from "@openzeppelin/contracts import {Counters} from "@openzeppelin/contracts/utils/Counters.sol"; -import "../interfaces/IManageableVault.sol"; -import "../interfaces/IMToken.sol"; -import "../interfaces/IDataFeed.sol"; +import {IManageableVault, TokenConfig, CommonVaultInitParams} from "../interfaces/IManageableVault.sol"; +import {IMToken} from "../interfaces/IMToken.sol"; +import {IDataFeed} from "../interfaces/IDataFeed.sol"; -import "../access/Greenlistable.sol"; -import "../access/Blacklistable.sol"; -import "../abstract/WithSanctionsList.sol"; +import {Greenlistable} from "../access/Greenlistable.sol"; +import {Blacklistable} from "../access/Blacklistable.sol"; +import {WithSanctionsList} from "../abstract/WithSanctionsList.sol"; -import "../libraries/DecimalsCorrectionLibrary.sol"; -import "../access/Pausable.sol"; +import {DecimalsCorrectionLibrary} from "../libraries/DecimalsCorrectionLibrary.sol"; +import {IMidasAccessControlManaged} from "../interfaces/IMidasAccessControlManaged.sol"; +import {PauseGuardsLibrary} from "../libraries/PauseGuardsLibrary.sol"; +import {WithMidasAccessControl} from "../access/WithMidasAccessControl.sol"; + +import {RateLimitLibrary} from "../libraries/RateLimitLibrary.sol"; +import {MidasInitializable} from "./MidasInitializable.sol"; /** * @title ManageableVault @@ -26,96 +31,101 @@ import "../access/Pausable.sol"; * @notice Contract with base Vault methods */ abstract contract ManageableVault is - Pausable, IManageableVault, Blacklistable, Greenlistable, WithSanctionsList { using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.UintSet; using DecimalsCorrectionLibrary for uint256; using SafeERC20 for IERC20; - using Counters for Counters.Counter; + using RateLimitLibrary for RateLimitLibrary.WindowRateLimits; /** - * @notice address that represents off-chain USD bank transfer + * @notice stable coin static rate 1:1 USD in 18 decimals */ - address public constant MANUAL_FULLFILMENT_TOKEN = address(0x0); + uint256 public constant STABLECOIN_RATE = 10**18; /** - * @notice stable coin static rate 1:1 USD in 18 decimals + * @notice 100 percent with base 100 + * @dev for example, 10% will be (10 * 100)% */ - uint256 public constant STABLECOIN_RATE = 10**18; + uint256 public constant ONE_HUNDRED_PERCENT = 100 * 100; /** - * @notice last request id + * @dev role that grants admin rights to the contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable */ - Counters.Counter public currentRequestId; + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _CONTRACT_ADMIN_ROLE; /** - * @notice 100 percent with base 100 - * @dev for example, 10% will be (10 * 100)% + * @dev role that grants greenlisted status to the contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable */ - uint256 public constant ONE_HUNDRED_PERCENT = 100 * 100; + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _GREENLISTED_ROLE; - uint256 public constant MAX_UINT = type(uint256).max; + /** + * @notice mapping, token address to token config + */ + mapping(address => TokenConfig) public tokensConfig; /** - * @notice mToken token + * @notice mapping, user address => is free frmo min amounts */ - IMToken public mToken; + mapping(address => bool) public isFreeFromMinAmount; /** - * @notice mToken data feed contract + * @notice address restriction with zero fees */ - IDataFeed public mTokenDataFeed; + mapping(address => bool) public override waivedFeeRestriction; /** - * @notice address to which tokens and mTokens will be sent + * @dev tokens that can be used as USD representation */ - address public tokensReceiver; + EnumerableSet.AddressSet internal _paymentTokens; /** - * @dev fee for initial operations 1% = 100 + * @notice instant rate limits state */ - uint256 public instantFee; + RateLimitLibrary.WindowRateLimits private _instantRateLimits; /** - * @dev daily limit for initial operations - * if user exceed this limit he will need - * to create requests + * @notice last request id */ - uint256 public instantDailyLimit; + uint256 public currentRequestId; /** - * @dev mapping days (number from 1970) to limit amount + * @notice next expected request id to process */ - mapping(uint256 => uint256) public dailyLimits; + uint256 public nextExpectedRequestIdToProcess; /** - * @notice address to which fees will be sent + * @notice max requestId that can be approved */ - address public feeReceiver; + uint256 public maxApproveRequestId; /** - * @notice variation tolerance of tokenOut rates for "safe" requests approve + * @notice mToken token */ - uint256 public variationTolerance; + IMToken public mToken; /** - * @notice address restriction with zero fees + * @notice mToken data feed contract */ - mapping(address => bool) public waivedFeeRestriction; + IDataFeed public mTokenDataFeed; /** - * @dev tokens that can be used as USD representation + * @notice address to which tokens and mTokens will be sent */ - EnumerableSet.AddressSet internal _paymentTokens; + address public tokensReceiver; /** - * @notice mapping, token address to token config + * @notice variation tolerance of tokenOut rates for "safe" requests approve */ - mapping(address => TokenConfig) public tokensConfig; + uint256 public variationTolerance; /** * @notice basic min operations amount @@ -123,9 +133,29 @@ abstract contract ManageableVault is uint256 public minAmount; /** - * @notice mapping, user address => is free frmo min amounts + * @dev fee for initial operations 1% = 100 */ - mapping(address => bool) public isFreeFromMinAmount; + uint256 public instantFee; + + /** + * @notice minimum instant fee + */ + uint256 public minInstantFee; + + /** + * @notice maximum instant fee + */ + uint256 public maxInstantFee; + + /** + * @notice maximum instant share value in basis points (100 = 1%) + */ + uint256 public maxInstantShare; + + /** + * @notice enforce sequential request processing flag + */ + bool public sequentialRequestProcessing; /** * @dev leaving a storage gap for futures updates @@ -133,67 +163,64 @@ abstract contract ManageableVault is uint256[50] private __gap; /** - * @dev checks that msg.sender do have a vaultRole() role + * @dev validate msg.sender and recipient access, validates if function is not paused + * @param recipient recipient address */ - modifier onlyVaultAdmin() { - _onlyRole(vaultRole(), msg.sender); + modifier validateUserAccess(address recipient) { + _validateUserAccess(msg.sender, recipient); _; } /** - * @dev upgradeable pattern contract`s initializer - * @param _ac address of MidasAccessControll contract - * @param _mTokenInitParams init params for mToken - * @param _receiversInitParams init params for receivers - * @param _instantInitParams init params for instant operations - * @param _sanctionsList address of sanctionsList contract - * @param _variationTolerance percent of prices diviation 1% = 100 - * @param _minAmount basic min amount for operations + * @notice constructor + * @param _contractAdminRole contract admin role + * @param _greenlistedRole greenlisted role + * @custom:oz-upgrades-unsafe-allow constructor */ - // solhint-disable func-name-mixedcase - function __ManageableVault_init( - address _ac, - MTokenInitParams calldata _mTokenInitParams, - ReceiversInitParams calldata _receiversInitParams, - InstantInitParams calldata _instantInitParams, - address _sanctionsList, - uint256 _variationTolerance, - uint256 _minAmount - ) internal onlyInitializing { - _validateAddress(_mTokenInitParams.mToken, false); - _validateAddress(_mTokenInitParams.mTokenDataFeed, false); - _validateAddress(_receiversInitParams.tokensReceiver, true); - _validateAddress(_receiversInitParams.feeReceiver, true); - require(_instantInitParams.instantDailyLimit > 0, "zero limit"); - _validateFee(_variationTolerance, true); - _validateFee(_instantInitParams.instantFee, false); - - mToken = IMToken(_mTokenInitParams.mToken); - __Pausable_init(_ac); - __Greenlistable_init_unchained(); - __Blacklistable_init_unchained(); - __WithSanctionsList_init_unchained(_sanctionsList); - - tokensReceiver = _receiversInitParams.tokensReceiver; - feeReceiver = _receiversInitParams.feeReceiver; - instantFee = _instantInitParams.instantFee; - instantDailyLimit = _instantInitParams.instantDailyLimit; - minAmount = _minAmount; - variationTolerance = _variationTolerance; - mTokenDataFeed = IDataFeed(_mTokenInitParams.mTokenDataFeed); + constructor(bytes32 _contractAdminRole, bytes32 _greenlistedRole) + MidasInitializable() + { + _CONTRACT_ADMIN_ROLE = _contractAdminRole; + _GREENLISTED_ROLE = _greenlistedRole; } /** - * @inheritdoc IManageableVault + * @dev upgradeable pattern contract`s initializer + * @param _commonVaultInitParams init params for common vault */ - function withdrawToken( - address token, - uint256 amount, - address withdrawTo - ) external onlyVaultAdmin { - IERC20(token).safeTransfer(withdrawTo, amount); + // solhint-disable func-name-mixedcase + function __ManageableVault_init( + CommonVaultInitParams calldata _commonVaultInitParams + ) internal onlyInitializing { + __WithMidasAccessControl_init(_commonVaultInitParams.ac); + __WithSanctionsList_init_unchained( + _commonVaultInitParams.sanctionsList + ); - emit WithdrawToken(msg.sender, token, withdrawTo, amount); + _validateAddress(_commonVaultInitParams.mToken, false); + _validateAddress(_commonVaultInitParams.mTokenDataFeed, false); + _validateAddress(_commonVaultInitParams.tokensReceiver, true); + _validateFee(_commonVaultInitParams.variationTolerance, true); + _validateFee(_commonVaultInitParams.instantFee, false); + _validateFee(_commonVaultInitParams.maxInstantShare, false); + + mToken = IMToken(_commonVaultInitParams.mToken); + + tokensReceiver = _commonVaultInitParams.tokensReceiver; + instantFee = _commonVaultInitParams.instantFee; + minAmount = _commonVaultInitParams.minAmount; + variationTolerance = _commonVaultInitParams.variationTolerance; + mTokenDataFeed = IDataFeed(_commonVaultInitParams.mTokenDataFeed); + sequentialRequestProcessing = _commonVaultInitParams + .sequentialRequestProcessing; + + maxInstantShare = _commonVaultInitParams.maxInstantShare; + maxApproveRequestId = _commonVaultInitParams.maxApproveRequestId; + + _setMinMaxInstantFee( + _commonVaultInitParams.minInstantFee, + _commonVaultInitParams.maxInstantFee + ); } /** @@ -205,8 +232,8 @@ abstract contract ManageableVault is uint256 tokenFee, uint256 allowance, bool stable - ) external onlyVaultAdmin { - require(_paymentTokens.add(token), "MV: already added"); + ) external onlyContractAdmin { + require(_paymentTokens.add(token), PaymentTokenAlreadyAdded(token)); _validateAddress(dataFeed, false); _validateFee(tokenFee, false); @@ -216,41 +243,30 @@ abstract contract ManageableVault is allowance: allowance, stable: stable }); - emit AddPaymentToken( - msg.sender, - token, - dataFeed, - tokenFee, - allowance, - stable - ); + emit AddPaymentToken(token, dataFeed, tokenFee, allowance, stable); } /** * @inheritdoc IManageableVault * @dev reverts if token is not presented */ - function removePaymentToken(address token) external onlyVaultAdmin { - require(_paymentTokens.remove(token), "MV: not exists"); + function removePaymentToken(address token) external onlyContractAdmin { + require(_paymentTokens.remove(token), PaymentTokenNotExists(token)); delete tokensConfig[token]; - emit RemovePaymentToken(token, msg.sender); + emit RemovePaymentToken(token); } /** * @inheritdoc IManageableVault - * @dev reverts if new allowance zero */ function changeTokenAllowance(address token, uint256 allowance) external - onlyVaultAdmin + onlyContractAdmin { - if (token != MANUAL_FULLFILMENT_TOKEN) { - _requireTokenExists(token); - } + _requireTokenExists(token); - require(allowance > 0, "MV: zero allowance"); tokensConfig[token].allowance = allowance; - emit ChangeTokenAllowance(token, msg.sender, allowance); + emit ChangeTokenAllowance(token, allowance); } /** @@ -259,98 +275,122 @@ abstract contract ManageableVault is */ function changeTokenFee(address token, uint256 fee) external - onlyVaultAdmin + onlyContractAdmin { _requireTokenExists(token); _validateFee(fee, false); tokensConfig[token].fee = fee; - emit ChangeTokenFee(token, msg.sender, fee); + emit ChangeTokenFee(token, fee); } /** * @inheritdoc IManageableVault - * @dev reverts if new tolerance zero + * @dev reverts if new tolerance > 100% */ - function setVariationTolerance(uint256 tolerance) external onlyVaultAdmin { - _validateFee(tolerance, true); + function setVariationTolerance(uint256 tolerance) + external + onlyContractAdmin + { + _validateFee(tolerance, false); variationTolerance = tolerance; - emit SetVariationTolerance(msg.sender, tolerance); + emit SetVariationTolerance(tolerance); } /** * @inheritdoc IManageableVault */ - function setMinAmount(uint256 newAmount) external onlyVaultAdmin { + function setMinAmount(uint256 newAmount) external onlyContractAdmin { minAmount = newAmount; - emit SetMinAmount(msg.sender, newAmount); + emit SetMinAmount(newAmount); } /** * @inheritdoc IManageableVault - * @dev reverts if account is already added */ - function addWaivedFeeAccount(address account) external onlyVaultAdmin { - require(!waivedFeeRestriction[account], "MV: already added"); - waivedFeeRestriction[account] = true; - emit AddWaivedFeeAccount(account, msg.sender); + function setWaivedFeeAccount(address account, bool enable) + external + onlyContractAdmin + { + require(waivedFeeRestriction[account] != enable, SameBoolValue(enable)); + waivedFeeRestriction[account] = enable; + emit SetWaivedFeeAccount(account, enable); } /** * @inheritdoc IManageableVault - * @dev reverts if account is already removed + * @dev reverts address zero or equal address(this) */ - function removeWaivedFeeAccount(address account) external onlyVaultAdmin { - require(waivedFeeRestriction[account], "MV: not found"); - waivedFeeRestriction[account] = false; - emit RemoveWaivedFeeAccount(account, msg.sender); + function setTokensReceiver(address receiver) external onlyContractAdmin { + _validateAddress(receiver, true); + + tokensReceiver = receiver; + + emit SetTokensReceiver(receiver); } /** * @inheritdoc IManageableVault - * @dev reverts address zero or equal address(this) */ - function setFeeReceiver(address receiver) external onlyVaultAdmin { - _validateAddress(receiver, true); - - feeReceiver = receiver; + function setInstantFee(uint256 newInstantFee) external onlyContractAdmin { + _validateFee(newInstantFee, false); - emit SetFeeReceiver(msg.sender, receiver); + instantFee = newInstantFee; + emit SetInstantFee(newInstantFee); } /** * @inheritdoc IManageableVault - * @dev reverts address zero or equal address(this) */ - function setTokensReceiver(address receiver) external onlyVaultAdmin { - _validateAddress(receiver, true); - - tokensReceiver = receiver; + function setMinMaxInstantFee( + uint256 newMinInstantFee, + uint256 newMaxInstantFee + ) external onlyContractAdmin { + _setMinMaxInstantFee(newMinInstantFee, newMaxInstantFee); + } - emit SetTokensReceiver(msg.sender, receiver); + /** + * @inheritdoc IManageableVault + */ + function setMaxInstantShare(uint256 newMaxInstantShare) + external + onlyContractAdmin + { + _validateFee(newMaxInstantShare, false); + maxInstantShare = newMaxInstantShare; + emit SetMaxInstantShare(newMaxInstantShare); } /** * @inheritdoc IManageableVault */ - function setInstantFee(uint256 newInstantFee) external onlyVaultAdmin { - _validateFee(newInstantFee, false); + function setMaxApproveRequestId(uint256 newMaxApproveRequestId) + external + onlyContractAdmin + { + maxApproveRequestId = newMaxApproveRequestId; + emit SetMaxApproveRequestId(newMaxApproveRequestId); + } - instantFee = newInstantFee; - emit SetInstantFee(msg.sender, newInstantFee); + /** + * @inheritdoc IManageableVault + */ + function setInstantLimitConfig(uint256 window, uint256 limit) + external + onlyContractAdmin + { + _instantRateLimits.setWindowLimit(window, limit); } /** * @inheritdoc IManageableVault */ - function setInstantDailyLimit(uint256 newInstantDailyLimit) + function removeInstantLimitConfig(uint256 window) external - onlyVaultAdmin + onlyContractAdmin { - require(newInstantDailyLimit > 0, "MV: limit zero"); - instantDailyLimit = newInstantDailyLimit; - emit SetInstantDailyLimit(msg.sender, newInstantDailyLimit); + _instantRateLimits.removeWindowLimit(window); } /** @@ -358,15 +398,39 @@ abstract contract ManageableVault is */ function freeFromMinAmount(address user, bool enable) external - onlyVaultAdmin + onlyContractAdmin { - require(isFreeFromMinAmount[user] != enable, "DV: already free"); + require(isFreeFromMinAmount[user] != enable, SameAddressValue(user)); isFreeFromMinAmount[user] = enable; emit FreeFromMinAmount(user, enable); } + /** + * @inheritdoc IManageableVault + */ + function setSequentialRequestProcessing(bool enforce) + external + onlyContractAdmin + { + require(sequentialRequestProcessing != enforce, SameBoolValue(enforce)); + sequentialRequestProcessing = enforce; + emit SetSequentialRequestProcessing(enforce); + } + + /** + * @inheritdoc IManageableVault + */ + function withdrawToken(address token, uint256 amount) + external + onlyContractAdmin + { + address withdrawTo = tokensReceiver; + IERC20(token).safeTransfer(withdrawTo, amount); + emit WithdrawToken(token, withdrawTo, amount); + } + /** * @notice returns array of stablecoins supported by the vault * can be called only from permissioned actor. @@ -377,29 +441,44 @@ abstract contract ManageableVault is } /** - * @notice AC role of vault administrator - * @return role bytes32 role + * @notice returns array of instant rate limit statuses + * @return statuses array of instant rate limit statuses */ - function vaultRole() public view virtual returns (bytes32); + function getInstantLimitStatuses() + external + view + returns ( + RateLimitLibrary.WindowRateLimitStatus[] memory /* statuses */ + ) + { + return _instantRateLimits.getWindowStatuses(); + } /** - * @inheritdoc WithSanctionsList + * @inheritdoc Greenlistable */ - function sanctionsListAdminRole() - public - view - virtual - override - returns (bytes32) - { - return vaultRole(); + function greenlistedRole() public view virtual override returns (bytes32) { + return _GREENLISTED_ROLE; } /** - * @inheritdoc Pausable + * @dev set minimum/maximum instant fee + * @param newMinInstantFee new minimum instant fee + * @param newMaxInstantFee new maximum instant fee */ - function pauseAdminRole() public view override returns (bytes32) { - return vaultRole(); + function _setMinMaxInstantFee( + uint256 newMinInstantFee, + uint256 newMaxInstantFee + ) private { + _validateFee(newMinInstantFee, false); + _validateFee(newMaxInstantFee, false); + require( + newMinInstantFee <= newMaxInstantFee, + InvalidMinMaxInstantFee(newMinInstantFee, newMaxInstantFee) + ); + minInstantFee = newMinInstantFee; + maxInstantFee = newMaxInstantFee; + emit SetMinMaxInstantFee(newMinInstantFee, newMaxInstantFee); } /** @@ -418,22 +497,17 @@ abstract contract ManageableVault is uint256 amount, uint256 tokenDecimals ) internal returns (uint256 transferAmount) { - transferAmount = amount.convertFromBase18(tokenDecimals); - - require( - amount == transferAmount.convertToBase18(tokenDecimals), - "MV: invalid rounding" - ); - - IERC20(token).safeTransferFrom(msg.sender, to, transferAmount); + return + _tokenTransferFromTo(token, msg.sender, to, amount, tokenDecimals); } /** - * @dev do safeTransferFrom on a given token + * @dev do safeTransfer or safeTransferFrom on a given token * and converts `amount` from base18 * to amount with a correct precision. * @param token address of token - * @param from address + * @param from address. If its address(this) the safeTransfer will be used + * instead of safeTransferFrom * @param to address * @param amount amount of `token` to transfer from `user` * @param tokenDecimals token decimals @@ -444,41 +518,68 @@ abstract contract ManageableVault is address to, uint256 amount, uint256 tokenDecimals - ) internal { - uint256 transferAmount = amount.convertFromBase18(tokenDecimals); + ) internal returns (uint256 transferAmount) { + if (amount == 0) return 0; + transferAmount = amount.convertFromBase18(tokenDecimals); + uint256 truncatedAmount = transferAmount.convertToBase18(tokenDecimals); require( - amount == transferAmount.convertToBase18(tokenDecimals), - "MV: invalid rounding" + amount == truncatedAmount, + InvalidRounding(amount, truncatedAmount) ); - IERC20(token).safeTransferFrom(from, to, transferAmount); + if (from == address(this)) { + IERC20(token).safeTransfer(to, transferAmount); + } else { + IERC20(token).safeTransferFrom(from, to, transferAmount); + } } /** - * @dev do safeTransfer on a given token - * and converts `amount` from base18 - * to amount with a correct precision. Sends tokens - * from `contract` to `user` - * @param token address of token - * @param to address of user - * @param amount amount of `token` to transfer from `user` (decimals 18) - * @param tokenDecimals token decimals + * @dev check if operation exceed daily limit and update limit data + * @param amount operation amount (decimals 18) */ - function _tokenTransferToUser( - address token, - address to, - uint256 amount, - uint256 tokenDecimals - ) internal { - uint256 transferAmount = amount.convertFromBase18(tokenDecimals); + function _requireAndUpdateLimit(uint256 amount) internal { + _instantRateLimits.consumeLimit(amount); + } + + /** + * @dev check if request id is sequential and update next expected request id to process + * @param requestId request id + * @param revertIfInvalid if true, reverts if request id is not sequential, otherwise returns false + * @return isValid true if request id is sequential or sequentialRequestProcessing is disabled + */ + function _validateAndUpdateNextRequestIdToProcess( + uint256 requestId, + bool revertIfInvalid + ) internal returns (bool isValid) { + isValid = true; + + if (!_validateMaxApproveRequestId(requestId, revertIfInvalid)) { + return false; + } + + uint256 _nextExpectedRequestIdToProcess = nextExpectedRequestIdToProcess; + + if ( + sequentialRequestProcessing && + requestId != _nextExpectedRequestIdToProcess + ) { + isValid = false; + } + + if (!revertIfInvalid && !isValid) { + return false; + } require( - amount == transferAmount.convertToBase18(tokenDecimals), - "MV: invalid rounding" + isValid, + InvalidRequestSequence(requestId, _nextExpectedRequestIdToProcess) ); - IERC20(token).safeTransfer(to, transferAmount); + if (requestId >= _nextExpectedRequestIdToProcess) { + nextExpectedRequestIdToProcess = requestId + 1; + } } /** @@ -495,20 +596,7 @@ abstract contract ManageableVault is * @param token address of token */ function _requireTokenExists(address token) internal view virtual { - require(_paymentTokens.contains(token), "MV: token not exists"); - } - - /** - * @dev check if operation exceed daily limit and update limit data - * @param amount operation amount (decimals 18) - */ - function _requireAndUpdateLimit(uint256 amount) internal { - uint256 currentDayNumber = block.timestamp / 1 days; - uint256 nextLimitAmount = dailyLimits[currentDayNumber] + amount; - - require(nextLimitAmount <= instantDailyLimit, "MV: exceed limit"); - - dailyLimits[currentDayNumber] = nextLimitAmount; + require(_paymentTokens.contains(token), UnknownPaymentToken(token)); } /** @@ -520,45 +608,63 @@ abstract contract ManageableVault is internal { uint256 prevAllowance = tokensConfig[token].allowance; - if (prevAllowance == MAX_UINT) return; + if (prevAllowance == type(uint256).max) return; - require(prevAllowance >= amount, "MV: exceed allowance"); + require( + prevAllowance >= amount, + AllowanceExceeded(prevAllowance, amount) + ); tokensConfig[token].allowance -= amount; } /** - * @dev returns calculated fee amount depends on parameters - * if additionalFee not zero, token fee replaced with additionalFee + * @dev returns calculated fee amount depends on the provided fee percent and amount + * @param feePercent fee percent + * @param amount amount of token (decimals 18) + + * @return feeAmount calculated fee amount + */ + function _getFeeAmount(uint256 feePercent, uint256 amount) + internal + pure + returns (uint256) + { + return (amount * feePercent) / ONE_HUNDRED_PERCENT; + } + + /** + * @dev returns calculated fee percent depends on parameters * @param sender sender address * @param token token address - * @param amount amount of token (decimals 18) * @param isInstant is instant operation - * @param additionalFee fee for fiat operations - * @return fee amount of input token + * + * @return feePercent calculated fee percent */ - function _getFeeAmount( + function _getFee( address sender, address token, - uint256 amount, - bool isInstant, - uint256 additionalFee - ) internal view returns (uint256) { + bool isInstant + ) internal view returns (uint256 feePercent) { if (waivedFeeRestriction[sender]) return 0; - uint256 feePercent; - if (additionalFee == 0) { - TokenConfig storage tokenConfig = tokensConfig[token]; - feePercent = tokenConfig.fee; - } else { - feePercent = additionalFee; - } + feePercent = tokensConfig[token].fee; if (isInstant) feePercent += instantFee; if (feePercent > ONE_HUNDRED_PERCENT) feePercent = ONE_HUNDRED_PERCENT; + } - return (amount * feePercent) / ONE_HUNDRED_PERCENT; + /** + * @dev validates instant fee is within the range of min/max instant fee + */ + function _validateInstantFee() internal view { + uint256 currentInstantFee = instantFee; + require( + currentInstantFee >= minInstantFee && + currentInstantFee <= maxInstantFee, + InstantFeeOutOfBounds(currentInstantFee) + ); } /** @@ -578,17 +684,104 @@ abstract contract ManageableVault is require( priceDifPercent <= variationTolerance, - "MV: exceed price diviation" + PriceVariationExceeded(priceDifPercent, variationTolerance) ); } - function _validateUserAccess(address user) + /** + * @dev validates that inputted mToken amount is >= minAmount() + * only if the `user` is not free from min amount + * @param user user address + * @param amountMToken amount of mToken + * @return isFreeFromMinAmount if the `user` is free from min amount + */ + function _validateMTokenAmount(address user, uint256 amountMToken) + internal + view + returns ( + bool /* isFreeFromMinAmount */ + ) + { + require(amountMToken > 0, InvalidAmount()); + + if (isFreeFromMinAmount[user]) { + return true; + } + + require( + amountMToken >= minAmount, + AmountLessThanMin(amountMToken, minAmount) + ); + + return false; + } + + /** + * @dev validate user access + * @param user user address + * @param validatePaused if true, validates if function is not paused + */ + function _validateUserAccess(address user, bool validatePaused) internal view onlyGreenlisted(user) onlyNotBlacklisted(user) onlyNotSanctioned(user) - {} + { + require(user != address(0), InvalidAddress(user)); + if (!validatePaused) return; + PauseGuardsLibrary.requireNotPaused(accessControl, msg.sig); + } + + /** + * @dev validate user access and validates if function is not paused + * @param user user address + * @param recipient recipient address + */ + function _validateUserAccess(address user, address recipient) + internal + view + { + _validateUserAccess(user, true); + + if (recipient != user) { + _validateUserAccess(recipient, false); + } + } + + /** + * @inheritdoc WithMidasAccessControl + */ + function contractAdminRole() + public + view + virtual + override + returns (bytes32) + { + return _CONTRACT_ADMIN_ROLE; + } + + /** + * @inheritdoc WithMidasAccessControl + */ + function _validateFunctionAccessWithTimelock( + bytes32 role, + uint32 overrideDelay, + bool roleIsFunctionOperator, + address account, + bool validateFunctionRole + ) internal view override { + PauseGuardsLibrary.requireFnNotPaused(accessControl, msg.sig); + + super._validateFunctionAccessWithTimelock( + role, + overrideDelay, + roleIsFunctionOperator, + account, + validateFunctionRole + ); + } /** * @dev convert value to inputted decimals precision @@ -610,8 +803,8 @@ abstract contract ManageableVault is * @param checkMin if need to check minimum */ function _validateFee(uint256 fee, bool checkMin) internal pure { - require(fee <= ONE_HUNDRED_PERCENT, "fee > 100%"); - if (checkMin) require(fee > 0, "fee == 0"); + require(fee <= ONE_HUNDRED_PERCENT, InvalidFee(fee)); + if (checkMin) require(fee > 0, InvalidFee(fee)); } /** @@ -620,8 +813,8 @@ abstract contract ManageableVault is * @param selfCheck check if address not address(this) */ function _validateAddress(address addr, bool selfCheck) internal view { - require(addr != address(0), "zero address"); - if (selfCheck) require(addr != address(this), "invalid address"); + require(addr != address(0), InvalidAddress(addr)); + if (selfCheck) require(addr != address(this), InvalidAddress(addr)); } /** @@ -642,4 +835,66 @@ abstract contract ManageableVault is return rate; } + + /** + * @dev gets and validates mToken rate + * @return mTokenRate mToken rate + */ + function _getMTokenRate() internal view returns (uint256 mTokenRate) { + mTokenRate = _getTokenRate(address(mTokenDataFeed), false); + _validateTokenRate(mTokenRate); + } + + /** + * @dev gets and validates pToken rate + * @param token address of pToken + * @return tokenRate token rate + */ + function _getPTokenRate(address token) + internal + view + returns (uint256 tokenRate) + { + TokenConfig storage tokenConfig = tokensConfig[token]; + tokenRate = _getTokenRate(tokenConfig.dataFeed, tokenConfig.stable); + _validateTokenRate(tokenRate); + } + + /** + * @dev validates that actual receive amount is greater than or equal to minimum receive amount + * @param actualReceiveAmount actual receive amount + * @param minReceiveAmount minimum receive amount + */ + function _requireSlippageNotExceeded( + uint256 actualReceiveAmount, + uint256 minReceiveAmount + ) internal pure { + require( + actualReceiveAmount >= minReceiveAmount, + SlippageExceeded(minReceiveAmount, actualReceiveAmount) + ); + } + + /** + * @dev validates that request id is less than or equal to max approve request id + * @param requestId request id + */ + function _validateMaxApproveRequestId( + uint256 requestId, + bool revertIfInvalid + ) internal view returns (bool isValid) { + isValid = requestId <= maxApproveRequestId; + + if (revertIfInvalid) { + require(isValid, RequestIdTooHigh(requestId, maxApproveRequestId)); + } + } + + /** + * @dev validates token rate + * @param rate token rate + */ + function _validateTokenRate(uint256 rate) private pure { + require(rate > 0, InvalidTokenRate(rate)); + } } diff --git a/contracts/abstract/MidasInitializable.sol b/contracts/abstract/MidasInitializable.sol index f7aa5bbe..f549598a 100644 --- a/contracts/abstract/MidasInitializable.sol +++ b/contracts/abstract/MidasInitializable.sol @@ -1,7 +1,8 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {StorageSlotUpgradeable as StorageSlot} from "@openzeppelin/contracts-upgradeable/utils/StorageSlotUpgradeable.sol"; /** * @title MidasInitializable @@ -11,8 +12,45 @@ import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol" * initialization of implementation contract */ abstract contract MidasInitializable is Initializable { + /** + * @notice error when the sender is not the proxy admin + */ + error SenderNotProxyAdmin(); + + /** + * @notice error when the address is invalid + * @param addr address + */ + error InvalidAddress(address addr); + + /** + * @notice modifier to check if the sender is the proxy admin + */ + modifier onlyProxyAdmin() { + _onlyProxyAdmin(); + _; + } + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } + + /** + * @notice function to check if the sender is the proxy admin + */ + function _onlyProxyAdmin() internal view virtual { + address admin = StorageSlot + .getAddressSlot( + 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 + ) + .value; + + // during proxy deployment and initialize calls admin would be zero + if (admin == address(0)) { + return; + } + + require(msg.sender == admin, SenderNotProxyAdmin()); + } } diff --git a/contracts/abstract/WithSanctionsList.sol b/contracts/abstract/WithSanctionsList.sol index b65a62ee..ca13f24d 100644 --- a/contracts/abstract/WithSanctionsList.sol +++ b/contracts/abstract/WithSanctionsList.sol @@ -1,9 +1,8 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "../interfaces/ISanctionsList.sol"; -import "../access/WithMidasAccessControl.sol"; -import "./MidasInitializable.sol"; +import {ISanctionsList} from "../interfaces/ISanctionsList.sol"; +import {WithMidasAccessControl} from "../access/WithMidasAccessControl.sol"; /** * @title WithSanctionsList @@ -23,13 +22,15 @@ abstract contract WithSanctionsList is WithMidasAccessControl { uint256[50] private __gap; /** - * @param caller function caller (msg.sender) * @param newSanctionsList new address of `sanctionsList` */ - event SetSanctionsList( - address indexed caller, - address indexed newSanctionsList - ); + event SetSanctionsList(address indexed newSanctionsList); + + /** + * @notice when user is sanctioned on sanctions list contract + * @param user user address + */ + error Sanctioned(address user); /** * @dev checks that a given `user` is not sanctioned @@ -39,24 +40,12 @@ abstract contract WithSanctionsList is WithMidasAccessControl { if (_sanctionsList != address(0)) { require( !ISanctionsList(_sanctionsList).isSanctioned(user), - "WSL: sanctioned" + Sanctioned(user) ); } _; } - /** - * @dev upgradeable pattern contract`s initializer - */ - // solhint-disable func-name-mixedcase - function __WithSanctionsList_init( - address _accesControl, - address _sanctionsList - ) internal onlyInitializing { - __WithMidasAccessControl_init(_accesControl); - __WithSanctionsList_init_unchained(_sanctionsList); - } - /** * @dev upgradeable pattern contract`s initializer unchained */ @@ -70,21 +59,13 @@ abstract contract WithSanctionsList is WithMidasAccessControl { /** * @notice updates `sanctionsList` address. - * can be called only from permissioned actor that have - * `sanctionsListAdminRole()` role * @param newSanctionsList new sanctions list address */ - function setSanctionsList(address newSanctionsList) external { - _onlyRole(sanctionsListAdminRole(), msg.sender); - + function setSanctionsList(address newSanctionsList) + external + onlyContractAdmin + { sanctionsList = newSanctionsList; - emit SetSanctionsList(msg.sender, newSanctionsList); + emit SetSanctionsList(newSanctionsList); } - - /** - * @notice AC role of sanctions list admin - * @dev address that have this role can use `setSanctionsList` - * @return role bytes32 role - */ - function sanctionsListAdminRole() public view virtual returns (bytes32); } diff --git a/contracts/access/Blacklistable.sol b/contracts/access/Blacklistable.sol index dd2801e9..0caaad69 100644 --- a/contracts/access/Blacklistable.sol +++ b/contracts/access/Blacklistable.sol @@ -1,7 +1,8 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "./WithMidasAccessControl.sol"; +import {WithMidasAccessControl} from "./WithMidasAccessControl.sol"; +import {MidasAuthLibrary} from "../libraries/MidasAuthLibrary.sol"; /** * @title Blacklistable @@ -16,8 +17,7 @@ abstract contract Blacklistable is WithMidasAccessControl { uint256[50] private __gap; /** - * @dev checks that a given `account` doesnt - * have BLACKLISTED_ROLE + * @dev checks that a given `account` doesnt have blacklisted role */ modifier onlyNotBlacklisted(address account) { _onlyNotBlacklisted(account); @@ -25,31 +25,13 @@ abstract contract Blacklistable is WithMidasAccessControl { } /** - * @dev upgradeable pattern contract`s initializer - * @param _accessControl MidasAccessControl contract address + * @dev checks that a given `account` doesnt have blacklisted role */ - // solhint-disable func-name-mixedcase - function __Blacklistable_init(address _accessControl) - internal - onlyInitializing - { - __WithMidasAccessControl_init(_accessControl); - __Blacklistable_init_unchained(); + function _onlyNotBlacklisted(address account) internal view { + MidasAuthLibrary.requireNotBlacklisted( + accessControl, + account, + MidasAuthLibrary.DEFAULT_BLACKLISTED_ROLE + ); } - - /** - * @dev upgradeable pattern contract`s initializer unchained - */ - // solhint-disable func-name-mixedcase - function __Blacklistable_init_unchained() internal onlyInitializing {} - - /** - * @dev checks that a given `account` doesnt - * have BLACKLISTED_ROLE - */ - function _onlyNotBlacklisted(address account) - internal - view - onlyNotRole(BLACKLISTED_ROLE, account) - {} } diff --git a/contracts/access/Greenlistable.sol b/contracts/access/Greenlistable.sol index 8c314c52..99088b2c 100644 --- a/contracts/access/Greenlistable.sol +++ b/contracts/access/Greenlistable.sol @@ -1,7 +1,8 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "./WithMidasAccessControl.sol"; +import {WithMidasAccessControl} from "./WithMidasAccessControl.sol"; +import {MidasAuthLibrary} from "../libraries/MidasAuthLibrary.sol"; /** * @title Greenlistable @@ -20,89 +21,39 @@ abstract contract Greenlistable is WithMidasAccessControl { */ uint256[50] private __gap; - event SetGreenlistEnable(address indexed sender, bool enable); - /** - * @dev checks that a given `account` - * have `greenlistedRole()` + * @param enable enable */ - modifier onlyGreenlisted(address account) { - if (greenlistEnabled) _onlyGreenlisted(account); - _; - } + event SetGreenlistEnable(bool enable); /** - * @dev checks that a given `account` - * have `greenlistedRole()` - * do the check even if greenlist check is off + * @dev checks that a given `account` has `greenlistedRole()` */ - modifier onlyAlwaysGreenlisted(address account) { - _onlyGreenlisted(account); + modifier onlyGreenlisted(address account) { + if (greenlistEnabled) { + MidasAuthLibrary.requireGreenlisted( + accessControl, + account, + greenlistedRole() + ); + } _; } - /** - * @dev upgradeable pattern contract`s initializer - * @param _accessControl MidasAccessControl contract address - */ - // solhint-disable func-name-mixedcase - function __Greenlistable_init(address _accessControl) - internal - onlyInitializing - { - __WithMidasAccessControl_init(_accessControl); - __Greenlistable_init_unchained(); - } - - /** - * @dev upgradeable pattern contract`s initializer unchained - */ - // solhint-disable func-name-mixedcase - function __Greenlistable_init_unchained() internal onlyInitializing {} - /** * @notice enable or disable greenlist. * can be called only from permissioned actor. * @param enable enable */ - function setGreenlistEnable(bool enable) external { - _onlyGreenlistToggler(msg.sender); - require(greenlistEnabled != enable, "GL: same enable status"); + function setGreenlistEnable(bool enable) external onlyContractAdmin { + require(greenlistEnabled != enable, SameBoolValue(enable)); greenlistEnabled = enable; - emit SetGreenlistEnable(msg.sender, enable); + emit SetGreenlistEnable(enable); } /** * @notice AC role of a greenlist * @return role bytes32 role */ - function greenlistedRole() public view virtual returns (bytes32) { - return GREENLISTED_ROLE; - } - - /** - * @notice AC role of a greenlist toggler - * @return role bytes32 role - */ - function greenlistTogglerRole() public view virtual returns (bytes32); - - /** - * @dev checks that a given `account` - * have a `greenlistedRole()` - */ - function _onlyGreenlisted(address account) - private - view - onlyRole(greenlistedRole(), account) - {} - - /** - * @dev checks that a given `account` - * have a `greenlistTogglerRole()` - */ - function _onlyGreenlistToggler(address account) - internal - view - onlyRole(greenlistTogglerRole(), account) - {} + function greenlistedRole() public view virtual returns (bytes32); } diff --git a/contracts/access/MidasAccessControl.sol b/contracts/access/MidasAccessControl.sol index 3c9e9e47..bbbe0f0c 100644 --- a/contracts/access/MidasAccessControl.sol +++ b/contracts/access/MidasAccessControl.sol @@ -1,10 +1,14 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {IAccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol"; -import "./MidasAccessControlRoles.sol"; -import "../abstract/MidasInitializable.sol"; +import {MidasInitializable} from "../abstract/MidasInitializable.sol"; +import {IMidasAccessControl} from "../interfaces/IMidasAccessControl.sol"; +import {MidasAuthLibrary} from "../libraries/MidasAuthLibrary.sol"; +import {IMidasAccessControlManaged} from "../interfaces/IMidasAccessControlManaged.sol"; +import {IMidasTimelockManager} from "../interfaces/IMidasTimelockManager.sol"; /** * @title MidasAccessControl @@ -12,56 +16,570 @@ import "../abstract/MidasInitializable.sol"; * @author RedDuck Software */ contract MidasAccessControl is + IMidasAccessControl, + IMidasAccessControlManaged, AccessControlUpgradeable, - MidasInitializable, - MidasAccessControlRoles + MidasInitializable { + /** + * @notice actor that can change green list statuses of addresses + */ + bytes32 public constant GREENLIST_OPERATOR_ROLE = + keccak256("GREENLIST_OPERATOR_ROLE"); + + /** + * @notice actor that can change black list statuses of addresses + */ + bytes32 public constant BLACKLIST_OPERATOR_ROLE = + keccak256("BLACKLIST_OPERATOR_ROLE"); + + /** + * @notice roles that are held by users + */ + mapping(bytes32 => bool) public isUserFacingRole; + + /** + * @dev Grant operators may call `setPermissionRoleMult` for the corresponding permission key. + */ + mapping(bytes32 => mapping(address => bool)) private _grantOperatorRoles; + + /** + * @dev Accounts allowed to call the scoped function on `targetContract`. + */ + mapping(bytes32 => mapping(address => bool)) private _permissionRoles; + + /** + * @dev timelock delay for each role + */ + mapping(bytes32 => uint32) private _roleTimelockDelays; + + /** + * @notice address of MidasAccessControlTimelockController contract + */ + address public timelockManager; + + /** + * @notice address of MidasAccessControlTimelockController contract + */ + address public pauseManager; + + /** + * @notice default delay for all of the roles + */ + uint32 public defaultDelay; + + /** + * @dev leaving a storage gap for futures updates + */ + uint256[50] private __gap; + + /** + * @dev validates that the msg.sender has the role + * @param role role to check access for + */ + modifier onlyRoleWithTimelock(bytes32 role) { + _validateRoleAccess(role); + _; + } + + /** + * @dev validates that the caller has the function role with timelock + * @param role base role to validate + * @param overrideDelay override delay for the invocation + */ + modifier onlyRoleDelayOverride(bytes32 role, uint32 overrideDelay) { + _validateRoleAccess(role, overrideDelay); + _; + } + /** * @notice upgradeable pattern contract`s initializer + * @param _defaultDelay default delay + * @param _userFacingRoles array of additional user facing roles */ - function initialize() external initializer { + function initialize( + uint32 _defaultDelay, + bytes32[] calldata _userFacingRoles + ) external { + _initializeV1(); + initializeV2(_defaultDelay, _userFacingRoles); + } + + /** + * @notice upgradeable pattern contract`s initializer + */ + function _initializeV1() private initializer { __AccessControl_init(); - _setupRoles(msg.sender); + _setupRoles(_msgSender()); + } + + /** + * @notice initializerV2. Initializes user facing roles + * @param _userFacingRoles array of additional user facing roles + */ + function initializeV2( + uint32 _defaultDelay, + bytes32[] calldata _userFacingRoles + ) public reinitializer(2) onlyProxyAdmin { + _validateDelay(_defaultDelay); + + defaultDelay = _defaultDelay; + + isUserFacingRole[MidasAuthLibrary.DEFAULT_BLACKLISTED_ROLE] = true; + isUserFacingRole[MidasAuthLibrary.DEFAULT_GREENLISTED_ROLE] = true; + + for (uint256 i = 0; i < _userFacingRoles.length; ++i) { + isUserFacingRole[_userFacingRoles[i]] = true; + } + } + + /** + * @notice initializes timelock manager. Moved to a searate initializer + * as its 2-way dependency between the contracts. + * @dev can be called only by DEFAULT_ADMIN_ROLE + * @param _timelockManager address of the timelock manager + * @param _pauseManager address of the pause manager + */ + function initializeRelationships( + address _timelockManager, + address _pauseManager + ) external { + _checkRole(DEFAULT_ADMIN_ROLE, _msgSender()); + + require(timelockManager == address(0), InvalidAddress(timelockManager)); + require(pauseManager == address(0), InvalidAddress(pauseManager)); + + require( + _timelockManager != address(0), + InvalidAddress(_timelockManager) + ); + require(_pauseManager != address(0), InvalidAddress(_pauseManager)); + + timelockManager = _timelockManager; + pauseManager = _pauseManager; } /** - * @notice grant multiple roles to multiple users - * in one transaction - * @dev length`s of 2 arays should match - * @param roles array of bytes32 roles - * @param addresses array of user addresses + * @inheritdoc IMidasAccessControl */ - function grantRoleMult(bytes32[] memory roles, address[] memory addresses) + function setDefaultDelay(uint32 _defaultDelay) external + onlyRoleDelayOverride(DEFAULT_ADMIN_ROLE, 2 days) { - require(roles.length == addresses.length, "MAC: mismatch arrays"); + _validateDelay(_defaultDelay); + defaultDelay = _defaultDelay; + emit SetDefaultDelay(_defaultDelay); + } + + /** + * @inheritdoc IMidasAccessControl + */ + function setRoleDelayMult(SetRoleDelayParams[] calldata params) + external + onlyRoleWithTimelock(DEFAULT_ADMIN_ROLE) + { + require(params.length > 0, EmptyArray()); - for (uint256 i = 0; i < roles.length; i++) { - _checkRole(getRoleAdmin(roles[i]), msg.sender); - _grantRole(roles[i], addresses[i]); + for (uint256 i = 0; i < params.length; ++i) { + _validateDelay(params[i].delay); + _roleTimelockDelays[params[i].role] = params[i].delay; } + + emit SetRoleDelays(params); } /** - * @notice revoke multiple roles from multiple users - * in one transaction - * @dev length`s of 2 arays should match - * @param roles array of bytes32 roles - * @param addresses array of user addresses + * @inheritdoc IMidasAccessControl */ - function revokeRoleMult(bytes32[] memory roles, address[] memory addresses) + function setUserFacingRoleMult(SetUserFacingRoleParams[] calldata params) external + onlyRoleWithTimelock(DEFAULT_ADMIN_ROLE) { - require(roles.length == addresses.length, "MAC: mismatch arrays"); - for (uint256 i = 0; i < roles.length; i++) { - _checkRole(getRoleAdmin(roles[i]), msg.sender); - _revokeRole(roles[i], addresses[i]); + require(params.length > 0, EmptyArray()); + + for (uint256 i = 0; i < params.length; ++i) { + SetUserFacingRoleParams calldata param = params[i]; + + // if value already set, skip and do not emit event + if (isUserFacingRole[param.role] == param.enabled) { + continue; + } + + isUserFacingRole[param.role] = param.enabled; + emit SetUserFacingRole(param.role, param.enabled); } } - //solhint-disable disable-next-line - function renounceRole(bytes32, address) public pure override { - revert("MAC: Forbidden"); + /** + * @inheritdoc IMidasAccessControl + */ + function setGrantOperatorRoleMult( + address targetContract, + SetGrantOperatorRoleParams[] calldata params + ) external { + require(params.length > 0, EmptyArray()); + + bytes32 masterRole = _getContractAdminRole(targetContract); + _validateRoleAccess(masterRole); + + MidasAuthLibrary.requireNotUserFacingRole(this, masterRole); + + for (uint256 i = 0; i < params.length; ++i) { + SetGrantOperatorRoleParams calldata param = params[i]; + + bytes32 operatorKey = grantOperatorRoleKey( + masterRole, + targetContract, + param.functionSelector + ); + + _validateAndUpdateDelay(operatorKey, param.delay); + + // if value already set, skip and do not emit event + if ( + _grantOperatorRoles[operatorKey][param.operator] == + param.enabled + ) { + continue; + } + + _grantOperatorRoles[operatorKey][param.operator] = param.enabled; + emit SetGrantOperatorRole( + masterRole, + targetContract, + param.operator, + param.functionSelector, + param.enabled + ); + } + } + + /** + * @inheritdoc IMidasAccessControl + */ + function setPermissionRoleMult( + bytes32 masterRole, + address targetContract, + bytes4 functionSelector, + uint32 delay, + SetPermissionRoleParams[] calldata params + ) public { + bytes32 operatorRoleKey = grantOperatorRoleKey( + masterRole, + targetContract, + functionSelector + ); + + _validateOperatorRoleAccess(masterRole, operatorRoleKey, _msgSender()); + + require(params.length > 0, EmptyArray()); + + bytes32 functionRoleKey = permissionRoleKey( + masterRole, + targetContract, + functionSelector + ); + + _validateAndUpdateDelay(functionRoleKey, delay); + + for (uint256 i = 0; i < params.length; ++i) { + SetPermissionRoleParams calldata param = params[i]; + + // if value already set, skip and do not emit event + if ( + _permissionRoles[functionRoleKey][param.account] == + param.enabled + ) { + continue; + } + + _permissionRoles[functionRoleKey][param.account] = param.enabled; + emit SetPermissionRole( + masterRole, + targetContract, + param.account, + functionSelector, + param.enabled + ); + } + } + + /** + * @inheritdoc IMidasAccessControl + */ + function setPermissionRoleMult( + address targetContract, + bytes4 functionSelector, + uint32 delay, + SetPermissionRoleParams[] calldata params + ) external { + bytes32 masterRole = _getContractAdminRole(targetContract); + setPermissionRoleMult( + masterRole, + targetContract, + functionSelector, + delay, + params + ); + } + + /** + * @inheritdoc IMidasAccessControl + */ + function grantRoleMult(GrantRoleMultParams[] calldata params) external { + require(params.length > 0, EmptyArray()); + + bytes32 adminRole = getRoleAdmin(params[0].role); + _validateRoleAccess(adminRole); + + for (uint256 i = 0; i < params.length; ++i) { + GrantRoleMultParams calldata param = params[i]; + + require( + getRoleAdmin(param.role) == adminRole, + RoleAdminMismatch(param.role, adminRole) + ); + _grantRole(param.role, param.account); + _validateAndUpdateDelay(param.role, param.delay); + } + } + + /** + * @inheritdoc IMidasAccessControl + */ + function revokeRoleMult(RevokeRoleMultParams[] calldata params) external { + require(params.length > 0, EmptyArray()); + + bytes32 adminRole = getRoleAdmin(params[0].role); + address actualSender = _validateRoleAccess(adminRole); + + for (uint256 i = 0; i < params.length; ++i) { + RevokeRoleMultParams calldata param = params[i]; + + require( + getRoleAdmin(param.role) == adminRole, + RoleAdminMismatch(param.role, adminRole) + ); + _validateRevokeRole(param.role, param.account, actualSender); + _revokeRole(param.role, param.account); + } + } + + /** + * @inheritdoc AccessControlUpgradeable + */ + function grantRole(bytes32 role, address account) + public + override(AccessControlUpgradeable, IAccessControlUpgradeable) + onlyRoleWithTimelock(getRoleAdmin(role)) + { + _grantRole(role, account); + } + + /** + * @inheritdoc IMidasAccessControl + */ + function grantRole( + bytes32 role, + address account, + uint32 delay + ) public { + grantRole(role, account); + _validateAndUpdateDelay(role, delay); + } + + /** + * @inheritdoc AccessControlUpgradeable + */ + function revokeRole(bytes32 role, address account) + public + override(AccessControlUpgradeable, IAccessControlUpgradeable) + { + address actualSender = _validateRoleAccess(getRoleAdmin(role)); + + _validateRevokeRole(role, account, actualSender); + _revokeRole(role, account); + } + + /** + * @inheritdoc IMidasAccessControl + */ + function setRoleAdmin(bytes32 role, bytes32 newAdminRole) + external + onlyRoleWithTimelock(getRoleAdmin(role)) + { + _setRoleAdmin(role, newAdminRole); + } + + // solhint-disable-next-line + /** + * @notice renouce role is forbidden + */ + function renounceRole(bytes32, address) + public + pure + override(AccessControlUpgradeable, IAccessControlUpgradeable) + { + revert Forbidden(); + } + + /** + * @inheritdoc IMidasAccessControl + */ + function isFunctionAccessGrantOperator( + bytes32 masterRole, + address targetContract, + bytes4 functionSelector, + address operator + ) external view returns (bool) { + bytes32 key = grantOperatorRoleKey( + masterRole, + targetContract, + functionSelector + ); + return isFunctionAccessGrantOperator(key, operator); + } + + /** + * @inheritdoc IMidasAccessControl + */ + function isFunctionAccessGrantOperator(bytes32 key, address operator) + public + view + returns (bool) + { + return _grantOperatorRoles[key][operator]; + } + + /** + * @inheritdoc IMidasAccessControl + */ + function hasFunctionPermission( + bytes32 masterRole, + address targetContract, + bytes4 functionSelector, + address account + ) external view returns (bool) { + bytes32 key = permissionRoleKey( + masterRole, + targetContract, + functionSelector + ); + return _permissionRoles[key][account]; + } + + /** + * @inheritdoc IMidasAccessControl + */ + function hasFunctionPermission(bytes32 key, address account) + external + view + returns (bool) + { + return _permissionRoles[key][account]; + } + + /** + * @inheritdoc IMidasAccessControl + */ + function permissionRoleKey( + bytes32 masterRole, + address targetContract, + bytes4 functionSelector + ) public pure returns (bytes32) { + return + _functionPermissionKey( + masterRole, + targetContract, + functionSelector, + "" + ); + } + + /** + * @inheritdoc IMidasAccessControl + */ + function grantOperatorRoleKey( + bytes32 masterRole, + address targetContract, + bytes4 functionSelector + ) public pure returns (bytes32) { + return + _functionPermissionKey( + masterRole, + targetContract, + functionSelector, + "operator" + ); + } + + /** + * @inheritdoc IMidasAccessControl + */ + function getRoleTimelockDelay(bytes32 role, uint32 overrideDelay) + public + view + returns ( + uint32, /* delay */ + bool /* isDefault */ + ) + { + uint32 delay = overrideDelay != MidasAuthLibrary.NULL_DELAY + ? overrideDelay + : _roleTimelockDelays[role]; + + uint32 actualDelay = delay == MidasAuthLibrary.NULL_DELAY + ? defaultDelay + : delay == MidasAuthLibrary.NO_DELAY + ? 0 + : delay; + + return (actualDelay, delay == 0); + } + + /** + * @inheritdoc IMidasAccessControlManaged + */ + function contractAdminRole() public view override returns (bytes32) { + return DEFAULT_ADMIN_ROLE; + } + + /** + * @dev validates and sets the delay for a role during the role granting + * @param role role id + */ + function _validateAndUpdateDelay(bytes32 role, uint32 delay) private { + if (delay == MidasAuthLibrary.NULL_DELAY) { + return; + } + + _validateDelay(delay); + + require(_roleTimelockDelays[role] == 0, DelayIsAlreadySet()); + + _roleTimelockDelays[role] = delay; + emit SetRoleDelay(role, delay); + } + + /** + * @dev calculates the base key for function permission mappings + * @param masterRole OZ role for the scope + */ + function _functionPermissionKey( + bytes32 masterRole, + address targetContract, + bytes4 functionSelector, + bytes memory additionalData + ) private pure returns (bytes32) { + return + keccak256( + abi.encode( + masterRole, + targetContract, + functionSelector, + additionalData + ) + ); } /** @@ -70,7 +588,171 @@ contract MidasAccessControl is function _setupRoles(address admin) private { _grantRole(DEFAULT_ADMIN_ROLE, admin); - _setRoleAdmin(BLACKLISTED_ROLE, BLACKLIST_OPERATOR_ROLE); - _setRoleAdmin(GREENLISTED_ROLE, GREENLIST_OPERATOR_ROLE); + _setRoleAdmin( + MidasAuthLibrary.DEFAULT_BLACKLISTED_ROLE, + BLACKLIST_OPERATOR_ROLE + ); + _setRoleAdmin( + MidasAuthLibrary.DEFAULT_GREENLISTED_ROLE, + GREENLIST_OPERATOR_ROLE + ); + } + + /** + * @dev validates that the delay is within the maximum delay + * @param delay delay to validate + */ + function _validateDelay(uint32 delay) private view { + MidasAuthLibrary.validateTimelockDelay(delay); + } + + /** + * @notice verifies that the role can be revoked + * @param role role to be revoked + * @param account account to be revoked + * @param actualSender account that actually verified for the function access + */ + function _validateRevokeRole( + bytes32 role, + address account, + address actualSender + ) private { + if (role == DEFAULT_ADMIN_ROLE && account == actualSender) { + revert CannotRevokeFromSelf(role, actualSender); + } + } + + /** + * @notice validates that the msg.sender with a role has access to the function + * @param role role to check access for + * @param overrideDelay override delay for the invocation + * @return actualAccount actual account that has access to the function + */ + function _validateRoleAccess(bytes32 role, uint32 overrideDelay) + internal + view + returns ( + address /* actualAccount */ + ) + { + return + MidasAuthLibrary.validateFunctionAccessWithTimelock( + this, + role, + overrideDelay, + false, + _msgSender(), + false + ); + } + + /** + * @notice validates that the msg.sender with a role has access to the function + * @param role role to check access for + * @return actualAccount actual account that has access to the function + */ + function _validateRoleAccess(bytes32 role) + internal + view + returns ( + address /* actualAccount */ + ) + { + return + MidasAuthLibrary.validateFunctionAccessWithTimelock( + this, + role, + MidasAuthLibrary.NULL_DELAY, + false, + _msgSender(), + false + ); + } + + /** + * @dev validates that the account with a master or operator role has access to the function + * selects a role with a shortest delay in case if has both roles + * @param masterRole master role + * @param operatorRole operator role + * @param account account to check access for + */ + function _validateOperatorRoleAccess( + bytes32 masterRole, + bytes32 operatorRole, + address account + ) internal view { + bytes32 role = _resolveOperatorRole(masterRole, operatorRole, account); + bool isOperatorRole = role == operatorRole; + + MidasAuthLibrary.validateFunctionAccessWithTimelock( + this, + role, + MidasAuthLibrary.NULL_DELAY, + isOperatorRole, + account, + false + ); + } + + /** + * @dev validates that the account has either operator or master role and uses the role with a shortest delay + * @param masterRole master role + * @param operatorRole operator role + * @param account account to check access for + */ + function _resolveOperatorRole( + bytes32 masterRole, + bytes32 operatorRole, + address account + ) internal view returns (bytes32) { + IMidasTimelockManager _timelockManager = IMidasTimelockManager( + timelockManager + ); + + // means that its a preflight call + if (account == address(_timelockManager)) { + account = MidasAuthLibrary.resolveProposer(msg.data); + } else if (account == _timelockManager.timelock()) { + account = _timelockManager.getOriginalProposer( + address(this), + msg.data + ); + } + + bool isOperator = isFunctionAccessGrantOperator(operatorRole, account); + bool hasMasterRole = hasRole(masterRole, account); + + if (!isOperator && !hasMasterRole) { + return operatorRole; + } + + if (!hasMasterRole) { + return operatorRole; + } + + if (!isOperator) { + return masterRole; + } + + return + MidasAuthLibrary.resolveAccessRole( + this, + masterRole, + operatorRole, + MidasAuthLibrary.NULL_DELAY + ); + } + + /** + * @notice gets the contract admin role for the target contract + * @param targetContract address of the target contract + * @return contractAdminRole contract admin role + */ + function _getContractAdminRole(address targetContract) + private + view + returns (bytes32) + { + return IMidasAccessControlManaged(targetContract).contractAdminRole(); } } diff --git a/contracts/access/MidasAccessControlRoles.sol b/contracts/access/MidasAccessControlRoles.sol deleted file mode 100644 index 295eb544..00000000 --- a/contracts/access/MidasAccessControlRoles.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; - -/** - * @title MidasAccessControlRoles - * @notice Base contract that stores all roles descriptors - * @author RedDuck Software - */ -abstract contract MidasAccessControlRoles { - /** - * @notice actor that can change green list statuses of addresses - */ - bytes32 public constant GREENLIST_OPERATOR_ROLE = - keccak256("GREENLIST_OPERATOR_ROLE"); - - /** - * @notice actor that can change black list statuses of addresses - */ - bytes32 public constant BLACKLIST_OPERATOR_ROLE = - keccak256("BLACKLIST_OPERATOR_ROLE"); - - /** - * @notice actor that is greenlisted - */ - bytes32 public constant GREENLISTED_ROLE = keccak256("GREENLISTED_ROLE"); - - /** - * @notice actor that is blacklisted - */ - bytes32 public constant BLACKLISTED_ROLE = keccak256("BLACKLISTED_ROLE"); -} diff --git a/contracts/access/MidasAccessControlTimelockController.sol b/contracts/access/MidasAccessControlTimelockController.sol new file mode 100644 index 00000000..98fe0299 --- /dev/null +++ b/contracts/access/MidasAccessControlTimelockController.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; + +import {TimelockControllerUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol"; +import {MidasInitializable} from "../abstract/MidasInitializable.sol"; + +/** + * @title MidasAccessControlTimelockController + * @notice TimelockController for Midas Protocol that is controlled by MidasTimelockManager + * @author RedDuck Software + */ +contract MidasAccessControlTimelockController is + TimelockControllerUpgradeable, + MidasInitializable +{ + /** + * @notice address of MidasTimelockManager contract + */ + address public timelockManager; + + /** + * @dev leaving a storage gap for futures updates + */ + uint256[50] private __gap; + + /** + * @notice upgradeable pattern contract`s initializer + * @param _timelockManager address of MidasTimelockManager contract + */ + function initialize(address _timelockManager) external initializer { + address[] memory managerArray = new address[](1); + managerArray[0] = _timelockManager; + + __TimelockController_init(0, managerArray, managerArray, address(0)); + + timelockManager = _timelockManager; + } +} diff --git a/contracts/access/MidasPauseManager.sol b/contracts/access/MidasPauseManager.sol new file mode 100644 index 00000000..3a019148 --- /dev/null +++ b/contracts/access/MidasPauseManager.sol @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; + +import {WithMidasAccessControl} from "./WithMidasAccessControl.sol"; +import {IMidasAccessControlManaged} from "../interfaces/IMidasAccessControlManaged.sol"; +import {IMidasPauseManager} from "../interfaces/IMidasPauseManager.sol"; +import {MidasAuthLibrary} from "../libraries/MidasAuthLibrary.sol"; +import {IMidasAccessControl} from "../interfaces/IMidasAccessControl.sol"; + +/** + * @title MidasPauseManager + * @notice Global manager for pausing and unpausing functions + * @author RedDuck Software + */ +contract MidasPauseManager is WithMidasAccessControl, IMidasPauseManager { + using MidasAuthLibrary for IMidasAccessControl; + + /** + * @notice static delay for setting pause delay + */ + uint32 public constant DELAY_FOR_SET_DELAY = 2 days; + + /** + * @dev admin role for the pause manager + */ + bytes32 private constant _PAUSE_ADMIN_ROLE = keccak256("PAUSE_ADMIN_ROLE"); + + /** + * @notice contract => paused status + */ + mapping(address => bool) public contractPaused; + + /** + * @notice contract => function id => paused status + */ + mapping(address => mapping(bytes4 => bool)) public contractFnPaused; + + /** + * @notice pause delay + */ + uint32 public pauseDelay; + + /** + * @notice unpause delay + */ + uint32 public unpauseDelay; + + /** + * @notice global paused status + */ + bool public globalPaused; + + /** + * @dev validates that caller has access to the `contractAddr` contract admin role + * overrides delay for the invocation with pause delay + * @param contractAddr address of the contract + */ + modifier onlyPausableContractAdminPause(address contractAddr) { + _validateContractAdminAccess(contractAddr, pauseDelay); + _; + } + + /** + * @dev validates that caller has access to the `contractAddr` contract admin role + * overrides delay for the invocation with unpause delay + * @param contractAddr address of the contract + */ + modifier onlyPausableContractAdminUnpause(address contractAddr) { + _validateContractAdminAccess(contractAddr, unpauseDelay); + _; + } + + /** + * @dev validates that caller has access to the pause admin role + * overrides delay for the invocation with pause delay + */ + modifier onlyAdminPause() { + _validateFunctionAccessWithTimelock( + contractAdminRole(), + pauseDelay, + false, + msg.sender, + true + ); + _; + } + + /** + * @dev validates that caller has access to the unpause admin role + * overrides delay for the invocation with unpause delay + */ + modifier onlyAdminUnpause() { + _validateFunctionAccessWithTimelock( + contractAdminRole(), + unpauseDelay, + false, + msg.sender, + true + ); + _; + } + + /** + * @notice upgradeable pattern contract`s initializer + * @param _accessControl address of MidasAccessControl contract + * @param _pauseDelay pause delay + * @param _unpauseDelay unpause delay + */ + function initialize( + address _accessControl, + uint32 _pauseDelay, + uint32 _unpauseDelay + ) external initializer { + __WithMidasAccessControl_init(_accessControl); + + _validateDelay(_pauseDelay); + _validateDelay(_unpauseDelay); + + pauseDelay = _pauseDelay; + unpauseDelay = _unpauseDelay; + } + + /** + * @inheritdoc IMidasPauseManager + */ + function setPauseDelay(uint32 _pauseDelay) + external + onlyRoleDelayOverride(contractAdminRole(), DELAY_FOR_SET_DELAY, true) + { + _validateDelay(_pauseDelay); + pauseDelay = _pauseDelay; + emit SetPauseDelay(_pauseDelay); + } + + /** + * @inheritdoc IMidasPauseManager + */ + function setUnpauseDelay(uint32 _unpauseDelay) + external + onlyRoleDelayOverride(contractAdminRole(), DELAY_FOR_SET_DELAY, true) + { + _validateDelay(_unpauseDelay); + unpauseDelay = _unpauseDelay; + emit SetUnpauseDelay(_unpauseDelay); + } + + /** + * @inheritdoc IMidasPauseManager + */ + function globalPause() external onlyAdminPause { + if (globalPaused) { + return; + } + + globalPaused = true; + emit GlobalPauseStatusChange(true); + } + + /** + * @inheritdoc IMidasPauseManager + */ + function globalUnpause() external onlyAdminUnpause { + if (!globalPaused) { + return; + } + + globalPaused = false; + emit GlobalPauseStatusChange(false); + } + + /** + * @inheritdoc IMidasPauseManager + */ + function bulkPauseContract(address[] calldata contractAddrs) + external + onlyAdminPause + { + for (uint256 i = 0; i < contractAddrs.length; ++i) { + _changeContratPauseStatus(contractAddrs[i], true); + } + } + + /** + * @inheritdoc IMidasPauseManager + */ + function bulkUnpauseContract(address[] calldata contractAddrs) + external + onlyAdminUnpause + { + for (uint256 i = 0; i < contractAddrs.length; ++i) { + _changeContratPauseStatus(contractAddrs[i], false); + } + } + + /** + * @inheritdoc IMidasPauseManager + */ + function bulkPauseContractFn( + address[] calldata contractAddrs, + bytes4[] calldata selectors + ) external onlyAdminPause { + for (uint256 i = 0; i < contractAddrs.length; ++i) { + address contractAddr = contractAddrs[i]; + + for (uint256 j = 0; j < selectors.length; ++j) { + bytes4 selector = selectors[j]; + + if (contractFnPaused[contractAddr][selector]) { + continue; + } + + contractFnPaused[contractAddr][selector] = true; + emit FnPauseStatusChange(contractAddr, selector, true); + } + } + } + + /** + * @inheritdoc IMidasPauseManager + */ + function bulkUnpauseContractFn( + address[] calldata contractAddrs, + bytes4[] calldata selectors + ) external onlyAdminUnpause { + for (uint256 i = 0; i < contractAddrs.length; ++i) { + address contractAddr = contractAddrs[i]; + + for (uint256 j = 0; j < selectors.length; ++j) { + bytes4 selector = selectors[j]; + + if (!contractFnPaused[contractAddr][selector]) { + continue; + } + + contractFnPaused[contractAddr][selector] = false; + emit FnPauseStatusChange(contractAddr, selector, false); + } + } + } + + /** + * @inheritdoc IMidasPauseManager + */ + function contractAdminPause(address contractAddr) + external + onlyPausableContractAdminPause(contractAddr) + { + _changeContratPauseStatus(contractAddr, true); + } + + /** + * @inheritdoc IMidasPauseManager + */ + function contractAdminUnpause(address contractAddr) + external + onlyPausableContractAdminUnpause(contractAddr) + { + _changeContratPauseStatus(contractAddr, false); + } + + /** + * @inheritdoc IMidasPauseManager + */ + function isPaused(address contractAddr, bytes4 selector) + external + view + returns (bool) + { + return + globalPaused || + contractPaused[contractAddr] || + isFunctionPaused(contractAddr, selector); + } + + /** + * @inheritdoc IMidasPauseManager + */ + function isFunctionPaused(address contractAddr, bytes4 selector) + public + view + returns (bool) + { + return contractFnPaused[contractAddr][selector]; + } + + /** + * @inheritdoc IMidasPauseManager + */ + function pauseAdminRole() public view returns (bytes32) { + return contractAdminRole(); + } + + /** + * @inheritdoc WithMidasAccessControl + */ + function contractAdminRole() public pure override returns (bytes32) { + return _PAUSE_ADMIN_ROLE; + } + + /** + * @dev changes the pause status of the `contractAddr` contract + * @param contractAddr address of the contract + * @param paused paused status + */ + function _changeContratPauseStatus(address contractAddr, bool paused) + private + { + if (contractPaused[contractAddr] == paused) { + return; + } + + contractPaused[contractAddr] = paused; + emit ContractPauseStatusChange(contractAddr, paused); + } + + /** + * @dev validates that caller has access to the `contractAddr` contract admin role + * @param contractAddr address of the contract + */ + function _validateContractAdminAccess( + address contractAddr, + uint32 overrideDelay + ) private view { + bytes32 role = _getPausableRole(contractAddr); + + _validateFunctionAccessWithTimelock( + role, + overrideDelay, + false, + msg.sender, + true + ); + } + + /** + * @dev validates that the delay is within the maximum delay + * @param delay delay to validate + */ + function _validateDelay(uint32 delay) private view { + MidasAuthLibrary.validateTimelockDelay(delay); + } + + /** + * @dev gets the pauser role and validate function role for the `contractAddr` contract + * @param contractAddr address of the contract + * @return role pauser role + */ + function _getPausableRole(address contractAddr) + private + view + returns (bytes32) + { + return IMidasAccessControlManaged(contractAddr).contractAdminRole(); + } +} diff --git a/contracts/access/MidasTimelockController.sol b/contracts/access/MidasTimelockController.sol index 279f9004..5c1094b7 100644 --- a/contracts/access/MidasTimelockController.sol +++ b/contracts/access/MidasTimelockController.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; diff --git a/contracts/access/MidasTimelockManager.sol b/contracts/access/MidasTimelockManager.sol new file mode 100644 index 00000000..11640ba9 --- /dev/null +++ b/contracts/access/MidasTimelockManager.sol @@ -0,0 +1,888 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; + +import {WithMidasAccessControl} from "../access/WithMidasAccessControl.sol"; +import {TimelockControllerUpgradeable as TimelockController} from "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol"; +import {IMidasTimelockManager, GetOperationStatusResult, TimelockOperationStatus} from "../interfaces/IMidasTimelockManager.sol"; +import {MidasAuthLibrary} from "../libraries/MidasAuthLibrary.sol"; +import {IMidasAccessControl} from "../interfaces/IMidasAccessControl.sol"; +import {IMidasPauseManager} from "../interfaces/IMidasPauseManager.sol"; +import {EnumerableSetUpgradeable as EnumerableSet} from "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; +import {ReentrancyGuardUpgradeable as ReentrancyGuard} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +/** + * @title MidasTimelockManager + * @notice Manages timelock scheduling, security council votes and operation details + * @author RedDuck Software + */ +contract MidasTimelockManager is + IMidasTimelockManager, + WithMidasAccessControl, + ReentrancyGuard +{ + using MidasAuthLibrary for IMidasAccessControl; + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.Bytes32Set; + + /** + * @dev internal storage for a timelock operation details + */ + struct TimelockOperationDetails { + EnumerableSet.AddressSet votersForExecution; + EnumerableSet.AddressSet votersForVeto; + uint256 councilVersion; + bytes32 dataHash; + TimelockOperationStatus status; + uint8 pauseReasonCode; + bool isSetCouncilOperation; + uint32 createdAt; + uint32 executionApprovedAt; + address operationProposer; + address pauser; + } + + /** + * @notice role that can pause timelock operations + */ + bytes32 public constant TIMELOCK_OPERATION_PAUSER_ROLE = + keccak256("TIMELOCK_OPERATION_PAUSER_ROLE"); + + /** + * @notice role that can set security council + */ + bytes32 public constant SECURITY_COUNCIL_MANAGER_ROLE = + keccak256("SECURITY_COUNCIL_MANAGER_ROLE"); + + /** + * @notice min security council members + */ + uint256 public constant SECURITY_COUNCIL_MIN_MEMBERS = 5; + + /** + * @notice max security council members + */ + uint256 public constant SECURITY_COUNCIL_MAX_MEMBERS = 15; + + /** + * @notice time after schedule when operation expires + */ + uint256 public constant EXPIRY_PERIOD = 45 days; + + /** + * @notice dispute period after execution approval + */ + uint256 public constant DISPUTE_PERIOD = 3 days; + + /** + * @notice hard cap for max pending operations per proposer + */ + uint256 public constant MAX_PENDING_OPERATIONS_PER_PROPOSER = 100; + + /** + * @inheritdoc IMidasTimelockManager + */ + mapping(bytes32 => uint256) public dataHashIndexes; + + /** + * @inheritdoc IMidasTimelockManager + */ + mapping(address => uint256) public proposerPendingOperationsCount; + + /** + * @dev set of security council addresses by version + */ + mapping(uint256 => EnumerableSet.AddressSet) private _securityCouncils; + + /** + * @dev mapping, operationId to operation details + */ + mapping(bytes32 => TimelockOperationDetails) private _operationDetails; + + /** + * @dev set of pending operation ids + */ + EnumerableSet.Bytes32Set private _pendingOperations; + + /** + * @inheritdoc IMidasTimelockManager + */ + address public timelock; + + /** + * @inheritdoc IMidasTimelockManager + */ + uint256 public maxPendingOperationsPerProposer; + + /** + * @inheritdoc IMidasTimelockManager + */ + uint256 public securityCouncilVersion; + + /** + * @inheritdoc IMidasTimelockManager + */ + bytes32 public pendingSetCouncilOperationId; + + /** + * @dev leaving a storage gap for futures updates + */ + uint256[50] private __gap; + + /** + * @dev validates that the caller has the contract admin role without timelock + * @param validateFunctionRole whether to validate the function role + */ + modifier onlyContractAdminNoTimelock(bool validateFunctionRole) { + _validateFunctionAccessWithoutTimelock( + contractAdminRole(), + false, + msg.sender, + validateFunctionRole + ); + _; + } + + /** + * @dev validates that the caller has the contract admin role without function role + */ + modifier onlyContractAdminNoFunctionRole() { + _validateFunctionAccessWithTimelock( + contractAdminRole(), + MidasAuthLibrary.NULL_DELAY, + false, + msg.sender, + false + ); + _; + } + + /** + * @notice Initializes the contract + * @param _accessControl MidasAccessControl address + * @param _maxPendingOperationsPerProposer max pending ops per proposer + * @param _initSecurityCouncil initial security council members + */ + function initialize( + address _accessControl, + uint256 _maxPendingOperationsPerProposer, + address[] calldata _initSecurityCouncil + ) external initializer { + __WithMidasAccessControl_init(_accessControl); + + _setMaxPendingOperationsPerProposer(_maxPendingOperationsPerProposer); + + _setSecurityCouncil(_initSecurityCouncil, securityCouncilVersion); + } + + /** + * @notice Initializes the timelock controller + * @param _timelock timelock controller address + */ + function initializeTimelock(address _timelock) + external + onlyContractAdminNoTimelock(false) + { + require(timelock == address(0), TimelockAlreadySet()); + require(_timelock != address(0), InvalidAddress(_timelock)); + timelock = _timelock; + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function setMaxPendingOperationsPerProposer( + uint256 _maxPendingOperationsPerProposer + ) external onlyContractAdminNoFunctionRole { + _setMaxPendingOperationsPerProposer(_maxPendingOperationsPerProposer); + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function setSecurityCouncil(address[] calldata members) + external + onlyRole(SECURITY_COUNCIL_MANAGER_ROLE, false) + { + if ( + msg.sender != timelock && pendingSetCouncilOperationId != bytes32(0) + ) { + revert PendingSetCouncilOperationExists(); + } + + uint256 version = securityCouncilVersion + 1; + securityCouncilVersion = version; + + _setSecurityCouncil(members, version); + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function bulkScheduleTimelockOperation( + ScheduleTimelockOperationParams[] calldata params + ) external { + for (uint256 i = 0; i < params.length; ++i) { + ScheduleTimelockOperationParams calldata param = params[i]; + _scheduleTimelockOperation(param.target, param.data); + } + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function scheduleTimelockOperation( + ScheduleTimelockOperationParams calldata params + ) external { + _scheduleTimelockOperation(params.target, params.data); + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function executeTimelockOperation(address target, bytes calldata data) + external + nonReentrant + onlyContractAdminNoTimelock(true) + { + TimelockController _timelock = TimelockController(payable(timelock)); + + ( + bytes32 operationId, + bytes32 dataHash, + uint256 dataHashIndex + ) = _getOperationId(_timelock, target, data); + + ( + TimelockOperationStatus status, + TimelockOperationDetails storage opDetails + ) = _getOperationStatus(operationId); + + require( + status == TimelockOperationStatus.Pending || + status == TimelockOperationStatus.ReadyToExecute, + UnexpectedOperationStatus(status) + ); + + require( + _timelock.isOperationReady(operationId), + TimelockOperationNotReady() + ); + + _timelock.execute(target, 0, data, bytes32(0), bytes32(dataHashIndex)); + + _resetPendingSetCouncilOperation(opDetails); + + // updating state after execution to be able to verify tx against current context + // in case of reentrancy timelock.execute will revert + opDetails.status = TimelockOperationStatus.Executed; + dataHashIndexes[dataHash] = dataHashIndex + 1; + --proposerPendingOperationsCount[opDetails.operationProposer]; + require(_pendingOperations.remove(operationId), OperationNotPending()); + + emit ExecuteTimelockOperation(msg.sender, operationId); + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function pauseOperation(bytes32 operationId, uint8 pauseReasonCode) + external + onlyRoleNoTimelock(TIMELOCK_OPERATION_PAUSER_ROLE, false) + { + require(_isPendingOperation(operationId), OperationNotPending()); + + ( + TimelockOperationStatus status, + TimelockOperationDetails storage opDetails + ) = _getOperationStatus(operationId); + + require( + status == TimelockOperationStatus.Pending, + UnexpectedOperationStatus(status) + ); + + uint256 councilVersion = securityCouncilVersion; + opDetails.status = TimelockOperationStatus.Paused; + opDetails.pauseReasonCode = pauseReasonCode; + opDetails.councilVersion = councilVersion; + opDetails.pauser = msg.sender; + + emit PauseTimelockOperation( + msg.sender, + operationId, + pauseReasonCode, + councilVersion + ); + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function voteForVeto(bytes32 operationId) external { + ( + TimelockOperationStatus status, + TimelockOperationDetails storage opDetails + ) = _getOperationStatus(operationId); + + require( + isInSecurityCouncil(opDetails.councilVersion, msg.sender), + NotInSecurityCouncil() + ); + + require( + status == TimelockOperationStatus.Paused || + status == TimelockOperationStatus.ApprovedExecution || + status == TimelockOperationStatus.ReadyToExecute, + UnexpectedOperationStatus(status) + ); + + require(opDetails.votersForVeto.add(msg.sender), AlreadyVoted()); + + if ( + opDetails.votersForVeto.length() >= + councilQuorum(opDetails.councilVersion) + ) { + opDetails.status = TimelockOperationStatus.ReadyToAbort; + } + + emit PausedProposalVoteCast(msg.sender, operationId, false); + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function voteForExecution(bytes32 operationId) external { + ( + TimelockOperationStatus status, + TimelockOperationDetails storage opDetails + ) = _getOperationStatus(operationId); + + require( + isInSecurityCouncil(opDetails.councilVersion, msg.sender), + NotInSecurityCouncil() + ); + + require( + status == TimelockOperationStatus.Paused, + UnexpectedOperationStatus(status) + ); + + require(opDetails.votersForExecution.add(msg.sender), AlreadyVoted()); + require(!opDetails.votersForVeto.contains(msg.sender), AlreadyVoted()); + + if ( + opDetails.votersForExecution.length() >= + councilQuorum(opDetails.councilVersion) + ) { + opDetails.status = TimelockOperationStatus.ApprovedExecution; + opDetails.executionApprovedAt = uint32(block.timestamp); + } + + emit PausedProposalVoteCast(msg.sender, operationId, true); + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function abortOperation(bytes32 operationId) external { + ( + TimelockOperationStatus status, + TimelockOperationDetails storage opDetails + ) = _getOperationStatus(operationId); + + uint256 dataHashIndex = dataHashIndexes[opDetails.dataHash]; + + require( + status == TimelockOperationStatus.ReadyToAbort || + status == TimelockOperationStatus.Expired, + UnexpectedOperationStatus(status) + ); + + _resetPendingSetCouncilOperation(opDetails); + + dataHashIndexes[opDetails.dataHash] = dataHashIndex + 1; + opDetails.status = TimelockOperationStatus.Aborted; + --proposerPendingOperationsCount[opDetails.operationProposer]; + require(_pendingOperations.remove(operationId), OperationNotPending()); + + TimelockController(payable(timelock)).cancel(operationId); + + emit AbortTimelockOperation(msg.sender, operationId, status); + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function getOriginalProposer(address target, bytes calldata data) + external + view + returns (address) + { + TimelockController _timelock = TimelockController(payable(timelock)); + (bytes32 operationId, , ) = _getOperationId(_timelock, target, data); + return _operationDetails[operationId].operationProposer; + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function councilQuorum(uint256 version) public view returns (uint8) { + return uint8(_securityCouncils[version].length()) / 2 + 1; + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function getCouncilMemberVoteStatus( + bytes32 operationId, + address councilMember + ) external view returns (bool votedForExecution, bool votedForVeto) { + (, TimelockOperationDetails storage opDetails) = _getOperationStatus( + operationId + ); + return ( + opDetails.votersForExecution.contains(councilMember), + opDetails.votersForVeto.contains(councilMember) + ); + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function getPendingOperations() external view returns (bytes32[] memory) { + return _pendingOperations.values(); + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function getOperationDetails(bytes32 operationId) + external + view + returns (GetOperationStatusResult memory result) + { + ( + TimelockOperationStatus status, + TimelockOperationDetails storage opDetails + ) = _getOperationStatus(operationId); + + result.status = status; + result.createdAt = opDetails.createdAt; + result.executionApprovedAt = opDetails.executionApprovedAt; + result.pauseReasonCode = opDetails.pauseReasonCode; + result.councilVersion = opDetails.councilVersion; + result.operationProposer = opDetails.operationProposer; + result.pauser = opDetails.pauser; + result.dataHash = opDetails.dataHash; + result.votesForExecution = uint8(opDetails.votersForExecution.length()); + result.votesForVeto = uint8(opDetails.votersForVeto.length()); + result.isSetCouncilOperation = opDetails.isSetCouncilOperation; + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function getOperationStatus(bytes32 operationId) + external + view + returns (TimelockOperationStatus status) + { + (status, ) = _getOperationStatus(operationId); + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function getOperationStatusRaw(bytes32 operationId) + external + view + returns (TimelockOperationStatus status) + { + return _operationDetails[operationId].status; + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function getSecurityCouncilMembers(uint256 version) + external + view + returns (address[] memory) + { + return _securityCouncils[version].values(); + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function getOperationId(address target, bytes calldata data) + external + view + returns (bytes32 operationId) + { + (operationId, , ) = _getOperationId( + TimelockController(payable(timelock)), + target, + data + ); + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function getTargetRole( + address target, + bytes calldata data, + address proposer + ) + public + view + returns ( + bytes32, /* role */ + uint32 /* overrideDelay */ + ) + { + (bool success, bytes memory err) = target.staticcall( + MidasAuthLibrary.appendProposer(data, proposer) + ); + require(!success, PreflightCallUnexpectedSuccess()); + bytes4 selector = _getFunctionSelector(data); + + ( + bytes32 role, + uint32 overrideDelay, + bool roleIsFunctionOperator, + bool validateFunctionRole + ) = _decodePreflightSucceededError(err); + + return ( + accessControl.validateFunctionAccess( + target, + role, + overrideDelay, + roleIsFunctionOperator, + proposer, + selector, + validateFunctionRole + ), + overrideDelay + ); + } + + /** + * @inheritdoc IMidasTimelockManager + */ + function isInSecurityCouncil(uint256 version, address account) + public + view + returns (bool) + { + return _securityCouncils[version].contains(account); + } + + /** + * @inheritdoc WithMidasAccessControl + */ + function contractAdminRole() public pure override returns (bytes32) { + return _DEFAULT_ADMIN_ROLE; + } + + /** + * @dev calculates and returns the actual status of an operation + * @param operationId operation id + * @return status actual operation status + * @return opDetails operation details + */ + function _getOperationStatus(bytes32 operationId) + private + view + returns ( + TimelockOperationStatus status, + TimelockOperationDetails storage opDetails + ) + { + opDetails = _operationDetails[operationId]; + status = opDetails.status; + + if ( + status != TimelockOperationStatus.Pending && + status != TimelockOperationStatus.Paused && + status != TimelockOperationStatus.ApprovedExecution + ) { + return (status, opDetails); + } + + uint256 passedSinceCreated = block.timestamp - opDetails.createdAt; + + if (passedSinceCreated >= EXPIRY_PERIOD) { + status = TimelockOperationStatus.Expired; + return (status, opDetails); + } + + if ( + status == TimelockOperationStatus.ApprovedExecution && + block.timestamp - opDetails.executionApprovedAt >= DISPUTE_PERIOD + ) { + status = TimelockOperationStatus.ReadyToExecute; + return (status, opDetails); + } + + return (status, opDetails); + } + + /** + * @dev schedules a timelock operation + * @param target target contract + * @param data operation data + */ + function _scheduleTimelockOperation(address target, bytes calldata data) + private + { + require(target != timelock, InvalidAddress(target)); + + address proposer = msg.sender; + + (bytes32 targetRole, uint32 overrideDelay) = getTargetRole( + target, + data, + proposer + ); + + (uint32 delay, ) = accessControl.getRoleTimelockDelay( + targetRole, + overrideDelay + ); + + require(delay != 0, NoTimelockDelayForRole()); + + TimelockController _timelock = TimelockController(payable(timelock)); + + ( + bytes32 operationId, + bytes32 dataHash, + uint256 dataHashIndex + ) = _getOperationId(_timelock, target, data); + + bool isSetCouncil = target == address(this) && + _getFunctionSelector(data) == this.setSecurityCouncil.selector; + + ++proposerPendingOperationsCount[proposer]; + + require( + proposerPendingOperationsCount[proposer] <= + maxPendingOperationsPerProposer, + TooManyPendingOperations() + ); + + TimelockOperationDetails storage opDetails = _operationDetails[ + operationId + ]; + + if (isSetCouncil) { + require( + pendingSetCouncilOperationId == bytes32(0), + PendingSetCouncilOperationExists() + ); + opDetails.isSetCouncilOperation = true; + pendingSetCouncilOperationId = operationId; + } + + opDetails.dataHash = dataHash; + opDetails.operationProposer = proposer; + opDetails.createdAt = uint32(block.timestamp); + opDetails.status = TimelockOperationStatus.Pending; + + require(_pendingOperations.add(operationId), OperationAlreadyPending()); + + _timelock.schedule( + target, + 0, + data, + bytes32(0), + bytes32(dataHashIndex), + delay + ); + + emit ScheduleTimelockOperation(proposer, operationId); + } + + /** + * @dev sets security council under a specific version + * @param members council member addresses + * @param version council version + */ + function _setSecurityCouncil(address[] calldata members, uint256 version) + private + { + require( + members.length >= SECURITY_COUNCIL_MIN_MEMBERS && + members.length <= SECURITY_COUNCIL_MAX_MEMBERS, + InvalidSecurityCouncilMembersLength() + ); + + EnumerableSet.AddressSet storage securityCouncil = _securityCouncils[ + version + ]; + + for (uint256 i = 0; i < members.length; ++i) { + require(members[i] != address(0), InvalidAddress(members[i])); + require( + securityCouncil.add(members[i]), + InvalidAddress(members[i]) + ); + } + + emit SetSecurityCouncil(version, members); + } + + /** + * @dev sets max pending operations per proposer + * @param _maxPendingOperationsPerProposer max pending operations per proposer + */ + function _setMaxPendingOperationsPerProposer( + uint256 _maxPendingOperationsPerProposer + ) private { + require( + _maxPendingOperationsPerProposer > 0 && + _maxPendingOperationsPerProposer <= + MAX_PENDING_OPERATIONS_PER_PROPOSER, + InvalidMaxPendingOperationsPerProposer() + ); + maxPendingOperationsPerProposer = _maxPendingOperationsPerProposer; + + emit SetMaxPendingOperationsPerProposer( + _maxPendingOperationsPerProposer + ); + } + + /** + * @dev resets the pending set-council operation + * if the operation is a set-council operation + * @param opDetails operation details + */ + function _resetPendingSetCouncilOperation( + TimelockOperationDetails storage opDetails + ) private { + if (!opDetails.isSetCouncilOperation) { + return; + } + + pendingSetCouncilOperationId = bytes32(0); + } + + /** + * @dev gets the timelock operation id for a given target and data + * @param _timelock timelock controller + * @param target target contract + * @param data operation data + * @return operationId operation id + * @return dataHash data hash + * @return dataHashIndex data hash index + */ + function _getOperationId( + TimelockController _timelock, + address target, + bytes calldata data + ) + private + view + returns ( + bytes32 operationId, + bytes32 dataHash, + uint256 dataHashIndex + ) + { + dataHash = _getDataHash(target, data); + dataHashIndex = dataHashIndexes[dataHash]; + + operationId = _timelock.hashOperation( + target, + 0, + data, + bytes32(0), + bytes32(dataHashIndex) + ); + } + + /** + * @dev checks if an operation is pending + * @param operationId operation id + * @return true if the operation is pending + */ + function _isPendingOperation(bytes32 operationId) + private + view + returns (bool) + { + return _pendingOperations.contains(operationId); + } + + /** + * @dev gets the function selector from operation data + * @param data operation data + * @return function selector + */ + function _getFunctionSelector(bytes calldata data) + private + pure + returns (bytes4) + { + return bytes4(data); + } + + /** + * @dev gets the keccak256 hash of a given target and data + * @param target target contract + * @param data operation data + * @return data hash + */ + function _getDataHash(address target, bytes calldata data) + private + pure + returns (bytes32) + { + // adding 0 as msg.value to make hash generation future-proof + return keccak256(abi.encodePacked(target, uint256(0), data)); + } + + /** + * @dev decodes a `RolePreflightSucceeded` error + * @param err error bytes + * @return role role + * @return overrideDelay override delay for the invocation + * @return roleIsFunctionOperator whether the role is a function operator role + * @return validateFunctionRole whether to validate the function role + */ + function _decodePreflightSucceededError(bytes memory err) + private + pure + returns ( + bytes32 role, + uint32 overrideDelay, + bool roleIsFunctionOperator, + bool validateFunctionRole + ) + { + require(err.length == 132, InvalidPreflightError(err)); + + bytes4 selector; + + // getting the selector of custom error + assembly { + selector := mload(add(err, 32)) + } + + // checking if the error is a RolePreflightSucceeded error + require( + selector == RolePreflightSucceeded.selector, + InvalidPreflightError(err) + ); + + assembly { + role := mload(add(err, 36)) + overrideDelay := mload(add(err, 68)) + roleIsFunctionOperator := mload(add(err, 100)) + validateFunctionRole := mload(add(err, 132)) + } + } +} diff --git a/contracts/access/Pausable.sol b/contracts/access/Pausable.sol deleted file mode 100644 index 289f311c..00000000 --- a/contracts/access/Pausable.sol +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.9; - -import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; -import "../access/WithMidasAccessControl.sol"; - -/** - * @title Pausable - * @notice Base contract that implements basic functions and modifiers - * with pause functionality - * @author RedDuck Software - */ -abstract contract Pausable is WithMidasAccessControl, PausableUpgradeable { - mapping(bytes4 => bool) public fnPaused; - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @param caller caller address (msg.sender) - * @param fn function id - */ - event PauseFn(address indexed caller, bytes4 fn); - - /** - * @param caller caller address (msg.sender) - * @param fn function id - */ - event UnpauseFn(address indexed caller, bytes4 fn); - - modifier whenFnNotPaused(bytes4 fn) { - _requireNotPaused(); - require(!fnPaused[fn], "Pausable: fn paused"); - _; - } - /** - * @dev checks that a given `account` - * has a determinedPauseAdminRole - */ - modifier onlyPauseAdmin() { - _onlyRole(pauseAdminRole(), msg.sender); - _; - } - - /** - * @dev upgradeable pattern contract`s initializer - * @param _accessControl MidasAccessControl contract address - */ - // solhint-disable-next-line func-name-mixedcase - function __Pausable_init(address _accessControl) internal onlyInitializing { - super.__Pausable_init(); - __WithMidasAccessControl_init(_accessControl); - } - - function pause() external onlyPauseAdmin { - _pause(); - } - - function unpause() external onlyPauseAdmin { - _unpause(); - } - - /** - * @dev pause specific function - * @param fn function id - */ - function pauseFn(bytes4 fn) external onlyPauseAdmin { - require(!fnPaused[fn], "Pausable: fn paused"); - fnPaused[fn] = true; - emit PauseFn(msg.sender, fn); - } - - /** - * @dev unpause specific function - * @param fn function id - */ - function unpauseFn(bytes4 fn) external onlyPauseAdmin { - require(fnPaused[fn], "Pausable: fn unpaused"); - fnPaused[fn] = false; - emit UnpauseFn(msg.sender, fn); - } - - /** - * @dev virtual function to determine pauseAdmin role - */ - function pauseAdminRole() public view virtual returns (bytes32); -} diff --git a/contracts/access/WithMidasAccessControl.sol b/contracts/access/WithMidasAccessControl.sol index 22a74154..4c668047 100644 --- a/contracts/access/WithMidasAccessControl.sol +++ b/contracts/access/WithMidasAccessControl.sol @@ -1,8 +1,10 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "./MidasAccessControl.sol"; -import "../abstract/MidasInitializable.sol"; +import {IMidasAccessControl} from "../interfaces/IMidasAccessControl.sol"; +import {MidasInitializable} from "../abstract/MidasInitializable.sol"; +import {MidasAuthLibrary} from "../libraries/MidasAuthLibrary.sol"; +import {IMidasAccessControlManaged} from "../interfaces/IMidasAccessControlManaged.sol"; /** * @title WithMidasAccessControl @@ -11,17 +13,20 @@ import "../abstract/MidasInitializable.sol"; */ abstract contract WithMidasAccessControl is MidasInitializable, - MidasAccessControlRoles + IMidasAccessControlManaged { + using MidasAuthLibrary for IMidasAccessControl; + /** * @notice admin role */ - bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + bytes32 internal constant _DEFAULT_ADMIN_ROLE = 0x00; /** * @notice MidasAccessControl contract address + * @custom:oz-retyped-from MidasAccessControl */ - MidasAccessControl public accessControl; + IMidasAccessControl public accessControl; /** * @dev leaving a storage gap for futures updates @@ -29,18 +34,73 @@ abstract contract WithMidasAccessControl is uint256[50] private __gap; /** - * @dev checks that given `address` have `role` + * @notice error when the value is the same as the previous value + * @param value value + */ + error SameBoolValue(bool value); + + /** + * @dev validates that the caller has the function role with timelock + * @param role base role to validate + * @param validateFunctionRole whether to validate the function role + */ + modifier onlyRole(bytes32 role, bool validateFunctionRole) { + _validateFunctionAccessWithTimelock( + role, + MidasAuthLibrary.NULL_DELAY, + false, + msg.sender, + validateFunctionRole + ); + _; + } + + /** + * @dev validates that the caller has the function role with timelock + * @param role base role to validate + * @param overrideDelay override delay for the invocation + * @param validateFunctionRole whether to validate the function role */ - modifier onlyRole(bytes32 role, address account) { - _onlyRole(role, account); + modifier onlyRoleDelayOverride( + bytes32 role, + uint32 overrideDelay, + bool validateFunctionRole + ) { + _validateFunctionAccessWithTimelock( + role, + overrideDelay, + false, + msg.sender, + validateFunctionRole + ); _; } /** - * @dev checks that given `address` do not have `role` + * @dev validates that the caller has the function role without timelock + * @param role base role to validate */ - modifier onlyNotRole(bytes32 role, address account) { - _onlyNotRole(role, account); + modifier onlyRoleNoTimelock(bytes32 role, bool validateFunctionRole) { + _validateFunctionAccessWithoutTimelock( + role, + false, + msg.sender, + validateFunctionRole + ); + _; + } + + /** + * @dev validates that the caller has the contract admin role or function operator role + */ + modifier onlyContractAdmin() { + _validateFunctionAccessWithTimelock( + contractAdminRole(), + MidasAuthLibrary.NULL_DELAY, + false, + msg.sender, + true + ); _; } @@ -52,21 +112,60 @@ abstract contract WithMidasAccessControl is internal onlyInitializing { - require(_accessControl != address(0), "zero address"); - accessControl = MidasAccessControl(_accessControl); + require(_accessControl != address(0), InvalidAddress(_accessControl)); + accessControl = IMidasAccessControl(_accessControl); } /** - * @dev checks that given `address` have `role` + * @dev validates that the function access is valid with timelock + * @param role base role to validate + * @param overrideDelay override delay for the invocation + * @param roleIsFunctionOperator whether the role is a function operator + * @param account account to validate + * @param validateFunctionRole whether to validate the function role */ - function _onlyRole(bytes32 role, address account) internal view { - require(accessControl.hasRole(role, account), "WMAC: hasnt role"); + function _validateFunctionAccessWithTimelock( + bytes32 role, + uint32 overrideDelay, + bool roleIsFunctionOperator, + address account, + bool validateFunctionRole + ) internal view virtual { + accessControl.validateFunctionAccessWithTimelock( + role, + overrideDelay, + roleIsFunctionOperator, + account, + validateFunctionRole + ); } /** - * @dev checks that given `address` do not have `role` + * @dev validates that the function access is valid without timelock + * @param role base role to validate + * @param roleIsFunctionOperator whether the role is a function operator + * @param account account to validate + * @param validateFunctionRole whether to validate the function role */ - function _onlyNotRole(bytes32 role, address account) internal view { - require(!accessControl.hasRole(role, account), "WMAC: has role"); + function _validateFunctionAccessWithoutTimelock( + bytes32 role, + bool roleIsFunctionOperator, + address account, + bool validateFunctionRole + ) internal view { + accessControl.validateFunctionAccess( + address(this), + role, + MidasAuthLibrary.NO_DELAY, + roleIsFunctionOperator, + account, + msg.sig, + validateFunctionRole + ); } + + /** + * @dev main admin role for the contract + */ + function contractAdminRole() public view virtual returns (bytes32); } diff --git a/contracts/feeds/CompositeDataFeed.sol b/contracts/feeds/CompositeDataFeed.sol index 81deddb0..f1a04c30 100644 --- a/contracts/feeds/CompositeDataFeed.sol +++ b/contracts/feeds/CompositeDataFeed.sol @@ -1,8 +1,11 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "../access/WithMidasAccessControl.sol"; -import "../interfaces/IDataFeed.sol"; +import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; + +import {MidasInitializable} from "../abstract/MidasInitializable.sol"; +import {WithMidasAccessControl} from "../access/WithMidasAccessControl.sol"; +import {IDataFeed} from "../interfaces/IDataFeed.sol"; /** * @title CompositeDataFeed @@ -13,6 +16,13 @@ import "../interfaces/IDataFeed.sol"; * @author RedDuck Software */ contract CompositeDataFeed is WithMidasAccessControl, IDataFeed { + /** + * @notice contract admin role + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _CONTRACT_ADMIN_ROLE; + /** * @notice price feed used as the numerator in the ratio calculation. * @dev typically represents the asset of interest (e.g., cbBTC/USD). @@ -40,6 +50,15 @@ contract CompositeDataFeed is WithMidasAccessControl, IDataFeed { */ uint256[50] private __gap; + /** + * @notice constructor + * @param _contractAdminRole contract admin role + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(bytes32 _contractAdminRole) MidasInitializable() { + _CONTRACT_ADMIN_ROLE = _contractAdminRole; + } + /** * @notice upgradeable pattern contract`s initializer * @param _ac MidasAccessControl contract address @@ -77,7 +96,7 @@ contract CompositeDataFeed is WithMidasAccessControl, IDataFeed { */ function changeNumeratorFeed(address _numeratorFeed) external - onlyRole(feedAdminRole(), msg.sender) + onlyContractAdmin { require(_numeratorFeed != address(0), "CDF: invalid address"); @@ -91,7 +110,7 @@ contract CompositeDataFeed is WithMidasAccessControl, IDataFeed { */ function changeDenominatorFeed(address _denominatorFeed) external - onlyRole(feedAdminRole(), msg.sender) + onlyContractAdmin { require(_denominatorFeed != address(0), "CDF: invalid address"); @@ -104,7 +123,7 @@ contract CompositeDataFeed is WithMidasAccessControl, IDataFeed { */ function setMinExpectedAnswer(uint256 _minExpectedAnswer) external - onlyRole(feedAdminRole(), msg.sender) + onlyContractAdmin { require( maxExpectedAnswer >= _minExpectedAnswer, @@ -120,7 +139,7 @@ contract CompositeDataFeed is WithMidasAccessControl, IDataFeed { */ function setMaxExpectedAnswer(uint256 _maxExpectedAnswer) external - onlyRole(feedAdminRole(), msg.sender) + onlyContractAdmin { require( _maxExpectedAnswer >= minExpectedAnswer, @@ -164,9 +183,9 @@ contract CompositeDataFeed is WithMidasAccessControl, IDataFeed { } /** - * @inheritdoc IDataFeed + * @inheritdoc WithMidasAccessControl */ - function feedAdminRole() public pure virtual override returns (bytes32) { - return DEFAULT_ADMIN_ROLE; + function contractAdminRole() public view override returns (bytes32) { + return _CONTRACT_ADMIN_ROLE; } } diff --git a/contracts/feeds/CompositeDataFeedMultiply.sol b/contracts/feeds/CompositeDataFeedMultiply.sol index 6654e93b..fa598aeb 100644 --- a/contracts/feeds/CompositeDataFeedMultiply.sol +++ b/contracts/feeds/CompositeDataFeedMultiply.sol @@ -1,7 +1,7 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "./CompositeDataFeed.sol"; +import {CompositeDataFeed} from "./CompositeDataFeed.sol"; /** * @title CompositeDataFeedMultiply @@ -13,6 +13,20 @@ import "./CompositeDataFeed.sol"; * @author RedDuck Software */ contract CompositeDataFeedMultiply is CompositeDataFeed { + /** + * @dev leaving a storage gap for futures updates + */ + uint256[50] private __gap; + + /** + * @notice constructor + * @param _contractAdminRole contract admin role + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(bytes32 _contractAdminRole) + CompositeDataFeed(_contractAdminRole) + {} + /** * @dev computes the composite price by multiplying the two feed values * @param firstFeedValue value from the first feed diff --git a/contracts/feeds/CustomAggregatorV3CompatibleFeed.sol b/contracts/feeds/CustomAggregatorV3CompatibleFeed.sol index 939d728a..015de309 100644 --- a/contracts/feeds/CustomAggregatorV3CompatibleFeed.sol +++ b/contracts/feeds/CustomAggregatorV3CompatibleFeed.sol @@ -1,12 +1,12 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; -import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; -import "../access/WithMidasAccessControl.sol"; -import "../libraries/DecimalsCorrectionLibrary.sol"; -import "../interfaces/IDataFeed.sol"; +import {MidasInitializable} from "../abstract/MidasInitializable.sol"; +import {WithMidasAccessControl} from "../access/WithMidasAccessControl.sol"; +import {DecimalsCorrectionLibrary} from "../libraries/DecimalsCorrectionLibrary.sol"; +import {IDataFeed} from "../interfaces/IDataFeed.sol"; /** * @title CustomAggregatorV3CompatibleFeed @@ -25,6 +25,13 @@ contract CustomAggregatorV3CompatibleFeed is uint80 answeredInRound; } + /** + * @notice contract admin role + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _CONTRACT_ADMIN_ROLE; + /** * @notice feed description */ @@ -56,6 +63,16 @@ contract CustomAggregatorV3CompatibleFeed is */ mapping(uint80 => RoundData) private _roundData; + /** + * @dev leaving a storage gap for futures updates + */ + uint256[50] private __gap; + + /** + * @param data data value + * @param roundId round id + * @param timestamp timestamp + */ event AnswerUpdated( int256 indexed data, uint256 indexed roundId, @@ -63,11 +80,17 @@ contract CustomAggregatorV3CompatibleFeed is ); /** - * @dev checks that msg.sender do have a feedAdminRole() role + * @param maxAnswerDeviation the new max answer deviation + */ + event MaxAnswerDeviationUpdated(uint256 indexed maxAnswerDeviation); + + /** + * @notice constructor + * @param _contractAdminRole contract admin role + * @custom:oz-upgrades-unsafe-allow constructor */ - modifier onlyAggregatorAdmin() { - _onlyRole(feedAdminRole(), msg.sender); - _; + constructor(bytes32 _contractAdminRole) MidasInitializable() { + _CONTRACT_ADMIN_ROLE = _contractAdminRole; } /** @@ -125,10 +148,10 @@ contract CustomAggregatorV3CompatibleFeed is /** * @notice sets the data for `latestRound` + 1 round id * @dev `_data` should be >= `minAnswer` and <= `maxAnswer`. - * Function should be called only from address with `feedAdminRole()` + * Function should be called only from address with `contractAdminRole()` * @param _data data value */ - function setRoundData(int256 _data) public onlyAggregatorAdmin { + function setRoundData(int256 _data) public onlyContractAdmin { require( _data >= minAnswer && _data <= maxAnswer, "CA: out of [min;max]" @@ -149,6 +172,23 @@ contract CustomAggregatorV3CompatibleFeed is emit AnswerUpdated(_data, roundId, block.timestamp); } + /** + * @notice sets the max answer deviation + * @dev the max answer deviation is the maximum allowed deviation from the latest price + * @param _maxAnswerDeviation the new max answer deviation + */ + function setMaxAnswerDeviation(uint256 _maxAnswerDeviation) + external + onlyContractAdmin + { + require( + _maxAnswerDeviation <= 100 * (10**decimals()), + "CA: !max deviation" + ); + maxAnswerDeviation = _maxAnswerDeviation; + emit MaxAnswerDeviationUpdated(_maxAnswerDeviation); + } + /** * @inheritdoc AggregatorV3Interface */ @@ -212,11 +252,10 @@ contract CustomAggregatorV3CompatibleFeed is } /** - * @dev describes a role, owner of which can update prices in this feed - * @return role descriptor + * @inheritdoc WithMidasAccessControl */ - function feedAdminRole() public view virtual returns (bytes32) { - return DEFAULT_ADMIN_ROLE; + function contractAdminRole() public view override returns (bytes32) { + return _CONTRACT_ADMIN_ROLE; } /** diff --git a/contracts/feeds/CustomAggregatorV3CompatibleFeedAdjusted.sol b/contracts/feeds/CustomAggregatorV3CompatibleFeedAdjusted.sol index 604dbe26..ba9a5de3 100644 --- a/contracts/feeds/CustomAggregatorV3CompatibleFeedAdjusted.sol +++ b/contracts/feeds/CustomAggregatorV3CompatibleFeedAdjusted.sol @@ -1,7 +1,7 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; /** * @title CustomAggregatorV3CompatibleFeedAdjusted diff --git a/contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol b/contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol index d95bd2d7..7d9b4442 100644 --- a/contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol +++ b/contracts/feeds/CustomAggregatorV3CompatibleFeedGrowth.sol @@ -1,10 +1,11 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; -import "../access/WithMidasAccessControl.sol"; -import "../interfaces/IAggregatorV3CompatibleFeedGrowth.sol"; +import {MidasInitializable} from "../abstract/MidasInitializable.sol"; +import {WithMidasAccessControl} from "../access/WithMidasAccessControl.sol"; +import {IAggregatorV3CompatibleFeedGrowth} from "../interfaces/IAggregatorV3CompatibleFeedGrowth.sol"; /** * @title CustomAggregatorV3CompatibleFeedGrowth @@ -25,6 +26,13 @@ contract CustomAggregatorV3CompatibleFeedGrowth is uint256 updatedAt; } + /** + * @notice contract admin role + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _CONTRACT_ADMIN_ROLE; + /** * @dev decimals of the aggregator */ @@ -88,11 +96,17 @@ contract CustomAggregatorV3CompatibleFeedGrowth is uint256[50] private __gap; /** - * @dev checks that msg.sender do have a feedAdminRole() role + * @dev having a second gap here to match with the gap of previous implementations + */ + uint256[50] private ___gap; + + /** + * @notice constructor + * @param _contractAdminRole contract admin role + * @custom:oz-upgrades-unsafe-allow constructor */ - modifier onlyAggregatorAdmin() { - _onlyRole(feedAdminRole(), msg.sender); - _; + constructor(bytes32 _contractAdminRole) MidasInitializable() { + _CONTRACT_ADMIN_ROLE = _contractAdminRole; } /** @@ -135,7 +149,7 @@ contract CustomAggregatorV3CompatibleFeedGrowth is /** * @inheritdoc IAggregatorV3CompatibleFeedGrowth */ - function setOnlyUp(bool _onlyUp) external override onlyAggregatorAdmin { + function setOnlyUp(bool _onlyUp) external override onlyContractAdmin { onlyUp = _onlyUp; emit OnlyUpUpdated(_onlyUp); } @@ -146,7 +160,7 @@ contract CustomAggregatorV3CompatibleFeedGrowth is function setMaxGrowthApr(int80 _maxGrowthApr) external override - onlyAggregatorAdmin + onlyContractAdmin { require(_maxGrowthApr >= minGrowthApr, "CAG: !max growth"); maxGrowthApr = _maxGrowthApr; @@ -159,13 +173,25 @@ contract CustomAggregatorV3CompatibleFeedGrowth is function setMinGrowthApr(int80 _minGrowthApr) external override - onlyAggregatorAdmin + onlyContractAdmin { require(_minGrowthApr <= maxGrowthApr, "CAG: !min growth"); minGrowthApr = _minGrowthApr; emit MinGrowthAprUpdated(_minGrowthApr); } + /** + * @inheritdoc IAggregatorV3CompatibleFeedGrowth + */ + function setMaxAnswerDeviation(uint256 _maxAnswerDeviation) + external + onlyContractAdmin + { + require(_maxAnswerDeviation <= 100 * _ONE, "CAG: !max deviation"); + maxAnswerDeviation = _maxAnswerDeviation; + emit MaxAnswerDeviationUpdated(_maxAnswerDeviation); + } + /** * @inheritdoc IAggregatorV3CompatibleFeedGrowth */ @@ -211,7 +237,7 @@ contract CustomAggregatorV3CompatibleFeedGrowth is int256 _data, uint256 _dataTimestamp, int80 _growthApr - ) public onlyAggregatorAdmin { + ) public onlyContractAdmin { require( _data >= minAnswer && _data <= maxAnswer, "CAG: out of [min;max]" @@ -391,11 +417,10 @@ contract CustomAggregatorV3CompatibleFeedGrowth is } /** - * @dev describes a role, owner of which can update prices in this feed - * @return role descriptor + * @inheritdoc WithMidasAccessControl */ - function feedAdminRole() public view virtual returns (bytes32) { - return DEFAULT_ADMIN_ROLE; + function contractAdminRole() public view override returns (bytes32) { + return _CONTRACT_ADMIN_ROLE; } /** diff --git a/contracts/feeds/DataFeed.sol b/contracts/feeds/DataFeed.sol index 48ce93b4..0710aee8 100644 --- a/contracts/feeds/DataFeed.sol +++ b/contracts/feeds/DataFeed.sol @@ -1,12 +1,12 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; -import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; -import "../access/WithMidasAccessControl.sol"; -import "../libraries/DecimalsCorrectionLibrary.sol"; -import "../interfaces/IDataFeed.sol"; +import {MidasInitializable} from "../abstract/MidasInitializable.sol"; +import {WithMidasAccessControl} from "../access/WithMidasAccessControl.sol"; +import {DecimalsCorrectionLibrary} from "../libraries/DecimalsCorrectionLibrary.sol"; +import {IDataFeed} from "../interfaces/IDataFeed.sol"; /** * @title DataFeed @@ -16,6 +16,13 @@ import "../interfaces/IDataFeed.sol"; contract DataFeed is WithMidasAccessControl, IDataFeed { using DecimalsCorrectionLibrary for uint256; + /** + * @notice contract admin role + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _CONTRACT_ADMIN_ROLE; + /** * @notice AggregatorV3Interface contract address */ @@ -41,6 +48,20 @@ contract DataFeed is WithMidasAccessControl, IDataFeed { */ uint256[50] private __gap; + /** + * @dev having a second gap here to match with the gap of previous implementations + */ + uint256[50] private ___gap; + + /** + * @notice constructor + * @param _contractAdminRole contract admin role + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(bytes32 _contractAdminRole) MidasInitializable() { + _CONTRACT_ADMIN_ROLE = _contractAdminRole; + } + /** * @notice upgradeable pattern contract`s initializer * @param _ac MidasAccessControl contract address @@ -77,10 +98,7 @@ contract DataFeed is WithMidasAccessControl, IDataFeed { * @notice updates `aggregator` address * @param _aggregator new AggregatorV3Interface contract address */ - function changeAggregator(address _aggregator) - external - onlyRole(feedAdminRole(), msg.sender) - { + function changeAggregator(address _aggregator) external onlyContractAdmin { require(_aggregator != address(0), "DF: invalid address"); aggregator = AggregatorV3Interface(_aggregator); @@ -90,10 +108,7 @@ contract DataFeed is WithMidasAccessControl, IDataFeed { * @dev updates `healthyDiff` value * @param _healthyDiff new value */ - function setHealthyDiff(uint256 _healthyDiff) - external - onlyRole(feedAdminRole(), msg.sender) - { + function setHealthyDiff(uint256 _healthyDiff) external onlyContractAdmin { require(_healthyDiff > 0, "DF: invalid diff"); healthyDiff = _healthyDiff; @@ -105,7 +120,7 @@ contract DataFeed is WithMidasAccessControl, IDataFeed { */ function setMinExpectedAnswer(int256 _minExpectedAnswer) external - onlyRole(feedAdminRole(), msg.sender) + onlyContractAdmin { require(_minExpectedAnswer > 0, "DF: invalid min exp. price"); require( @@ -122,7 +137,7 @@ contract DataFeed is WithMidasAccessControl, IDataFeed { */ function setMaxExpectedAnswer(int256 _maxExpectedAnswer) external - onlyRole(feedAdminRole(), msg.sender) + onlyContractAdmin { require(_maxExpectedAnswer > 0, "DF: invalid max exp. price"); require( @@ -141,10 +156,10 @@ contract DataFeed is WithMidasAccessControl, IDataFeed { } /** - * @inheritdoc IDataFeed + * @inheritdoc WithMidasAccessControl */ - function feedAdminRole() public pure virtual override returns (bytes32) { - return DEFAULT_ADMIN_ROLE; + function contractAdminRole() public view override returns (bytes32) { + return _CONTRACT_ADMIN_ROLE; } /** diff --git a/contracts/interfaces/IAggregatorV3CompatibleFeedGrowth.sol b/contracts/interfaces/IAggregatorV3CompatibleFeedGrowth.sol index 874ab7d5..f6edbe26 100644 --- a/contracts/interfaces/IAggregatorV3CompatibleFeedGrowth.sol +++ b/contracts/interfaces/IAggregatorV3CompatibleFeedGrowth.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +pragma solidity 0.8.34; -import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; /** * @title IAggregatorV3CompatibleFeedGrowth @@ -23,6 +23,11 @@ interface IAggregatorV3CompatibleFeedGrowth is AggregatorV3Interface { int80 growthApr ); + /** + * @param maxAnswerDeviation the new max answer deviation + */ + event MaxAnswerDeviationUpdated(uint256 indexed maxAnswerDeviation); + /** * @notice emitted when max growth apr is updated * @@ -65,6 +70,13 @@ interface IAggregatorV3CompatibleFeedGrowth is AggregatorV3Interface { */ function setMinGrowthApr(int80 _minGrowthApr) external; + /** + * @notice sets the max answer deviation + * @dev the max answer deviation is the maximum allowed deviation from the latest price + * @param _maxAnswerDeviation the new max answer deviation in % + */ + function setMaxAnswerDeviation(uint256 _maxAnswerDeviation) external; + /** * @notice works as `setRoundData()`, but also checks the * deviation with the lattest submitted data diff --git a/contracts/interfaces/IDataFeed.sol b/contracts/interfaces/IDataFeed.sol index 4edc4381..77e1a4fc 100644 --- a/contracts/interfaces/IDataFeed.sol +++ b/contracts/interfaces/IDataFeed.sol @@ -1,11 +1,7 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity 0.8.34; -import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; -import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; - -import "../access/WithMidasAccessControl.sol"; -import "../libraries/DecimalsCorrectionLibrary.sol"; +import {DecimalsCorrectionLibrary} from "../libraries/DecimalsCorrectionLibrary.sol"; /** * @title IDataFeed @@ -18,10 +14,4 @@ interface IDataFeed { * @return answer fetched aggregator answer */ function getDataInBase18() external view returns (uint256 answer); - - /** - * @dev describes a role, owner of which can manage this feed - * @return role descriptor - */ - function feedAdminRole() external view returns (bytes32); } diff --git a/contracts/interfaces/IDepositVault.sol b/contracts/interfaces/IDepositVault.sol index 309687c2..302db257 100644 --- a/contracts/interfaces/IDepositVault.sol +++ b/contracts/interfaces/IDepositVault.sol @@ -1,24 +1,42 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity 0.8.34; -import "./IManageableVault.sol"; +import {IManageableVault, RequestStatus} from "./IManageableVault.sol"; /** * @notice Mint request scruct - * @param sender user address who create - * @param tokenIn tokenIn address - * @param status request status - * @param depositedUsdAmount amout USD, tokenIn -> USD - * @param usdAmountWithoutFees amout USD, tokenIn - fees -> USD - * @param tokenOutRate rate of mToken at request creation time */ struct Request { - address sender; + /// @notice address who will receive the mTokens + address recipient; + /// @notice tokenIn address address tokenIn; + /// @notice request status RequestStatus status; + /// @notice amout USD, tokenIn -> USD uint256 depositedUsdAmount; + /// @notice amout USD, tokenIn - fees -> USD uint256 usdAmountWithoutFees; + /// @notice rate of mToken at request creation time uint256 tokenOutRate; + /// @notice amount of tokenIn that was deposited instantly in USD + uint256 depositedInstantUsdAmount; + /// @notice approved tokenOut rate + uint256 approvedTokenOutRate; + /// @notice amount of mToken that was minted + uint256 amountMToken; +} + +/** + * @notice Deposit vault init params + */ +struct DepositVaultInitParams { + /// @notice minimal USD amount for first user`s deposit + uint256 minMTokenAmountForFirstDeposit; + /// @notice max supply cap value in mToken + uint256 maxSupplyCap; + /// @notice max amount per request in mToken + uint256 maxAmountPerRequest; } /** @@ -27,144 +45,125 @@ struct Request { */ interface IDepositVault is IManageableVault { /** - * @param caller function caller (msg.sender) * @param newValue new min amount to deposit value */ - event SetMinMTokenAmountForFirstDeposit( - address indexed caller, - uint256 newValue - ); + event SetMinMTokenAmountForFirstDeposit(uint256 newValue); /** - * @param caller function caller (msg.sender) * @param newValue new max supply cap value */ - event SetMaxSupplyCap(address indexed caller, uint256 newValue); + event SetMaxSupplyCap(uint256 newValue); /** - * @param user function caller (msg.sender) - * @param tokenIn address of tokenIn - * @param amountUsd amount of tokenIn converted to USD - * @param amountToken amount of tokenIn - * @param fee fee amount in tokenIn - * @param minted amount of minted mTokens - * @param referrerId referrer id + * @param newValue new max amount per request */ - event DepositInstant( - address indexed user, - address indexed tokenIn, - uint256 amountUsd, - uint256 amountToken, - uint256 fee, - uint256 minted, - bytes32 referrerId - ); + event SetMaxAmountPerRequest(uint256 newValue); /** * @param user function caller (msg.sender) * @param tokenIn address of tokenIn * @param recipient address that receives the mTokens - * @param amountUsd amount of tokenIn converted to USD - * @param amountToken amount of tokenIn - * @param fee fee amount in tokenIn - * @param minted amount of minted mTokens + * @param amountTokenIn amount of tokenIn + * @param feeAmount fee amount in tokenIn + * @param amountMToken amount of minted mTokens + * @param mTokenRate mToken rate + * @param tokenInRate tokenIn rate * @param referrerId referrer id */ - event DepositInstantWithCustomRecipient( + event DepositInstant( address indexed user, address indexed tokenIn, - address recipient, - uint256 amountUsd, - uint256 amountToken, - uint256 fee, - uint256 minted, + address indexed recipient, + uint256 amountTokenIn, + uint256 feeAmount, + uint256 amountMToken, + uint256 mTokenRate, + uint256 tokenInRate, bytes32 referrerId ); /** * @param requestId mint request id * @param user function caller (msg.sender) + * @param recipient address that receives the mTokens * @param tokenIn address of tokenIn - * @param amountToken amount of tokenIn - * @param amountUsd amount of tokenIn converted to USD - * @param fee fee amount in tokenIn - * @param tokenOutRate mToken rate + * @param amountTokenIn amount of tokenIn + * @param amountTokenInInstant amount of tokenIn that was deposited instantly + * @param feeAmount fee amount in tokenIn + * @param mTokenRate mToken rate + * @param tokenInRate tokenIn rate * @param referrerId referrer id */ event DepositRequest( uint256 indexed requestId, address indexed user, address indexed tokenIn, - uint256 amountToken, - uint256 amountUsd, - uint256 fee, - uint256 tokenOutRate, + address recipient, + uint256 amountTokenIn, + uint256 amountTokenInInstant, + uint256 feeAmount, + uint256 mTokenRate, + uint256 tokenInRate, bytes32 referrerId ); /** * @param requestId mint request id - * @param user function caller (msg.sender) - * @param recipient address that receives the mTokens - * @param tokenIn address of tokenIn - * @param amountToken amount of tokenIn - * @param amountUsd amount of tokenIn converted to USD - * @param fee fee amount in tokenIn - * @param tokenOutRate mToken rate - * @param referrerId referrer id + * @param newOutRate mToken rate inputted by admin + * @param isSafe if true, approval is safe + * @param isAvgRate if true, newOutRate is avg rate */ - event DepositRequestWithCustomRecipient( + event ApproveRequest( uint256 indexed requestId, - address indexed user, - address indexed tokenIn, - address recipient, - uint256 amountToken, - uint256 amountUsd, - uint256 fee, - uint256 tokenOutRate, - bytes32 referrerId + uint256 newOutRate, + bool isSafe, + bool isAvgRate ); /** * @param requestId mint request id - * @param newOutRate mToken rate inputted by admin */ - event ApproveRequest(uint256 indexed requestId, uint256 newOutRate); + event RejectRequest(uint256 indexed requestId); /** - * @param requestId mint request id - * @param newOutRate mToken rate inputted by admin + * @notice first deposit mint amount is below minimum + * @param amountMTokenWithoutFee mint amount after fee (decimals 18) + * @param minAmount minimum first deposit mint amount */ - event SafeApproveRequest(uint256 indexed requestId, uint256 newOutRate); + error LessThanMinAmountFirstDeposit( + uint256 amountMTokenWithoutFee, + uint256 minAmount + ); /** - * @param requestId mint request id - * @param user address of user + * @notice when token supply cap is exceeded */ - event RejectRequest(uint256 indexed requestId, address indexed user); + error SupplyCapExceeded(); /** - * @param user address that was freed from min deposit check + * @notice when max amount per request is exceeded + * @param estimatedMintAmount estimated mint amount */ - event FreeFromMinDeposit(address indexed user); + error MaxAmountPerRequestExceeded(uint256 estimatedMintAmount); /** * @notice depositing proccess with auto mint if * account fit daily limit and token allowance. * Transfers token from the user. - * Transfers fee in tokenIn to feeReceiver. + * Transfers fee in tokenIn to tokensReceiver. * Mints mToken to user. * @param tokenIn address of tokenIn * @param amountToken amount of `tokenIn` that will be taken from user (decimals 18) * @param minReceiveAmount minimum expected amount of mToken to receive (decimals 18) * @param referrerId referrer id + * @return mintAmount amount of mToken that was minted */ function depositInstant( address tokenIn, uint256 amountToken, uint256 minReceiveAmount, bytes32 referrerId - ) external; + ) external returns (uint256); /** * @notice Does the same as original `depositInstant` but allows specifying a custom tokensReceiver address. @@ -173,6 +172,7 @@ interface IDepositVault is IManageableVault { * @param minReceiveAmount minimum expected amount of mToken to receive (decimals 18) * @param referrerId referrer id * @param tokensReceiver address to receive the tokens (instead of msg.sender) + * @return mintAmount amount of mToken that was minted */ function depositInstant( address tokenIn, @@ -180,13 +180,13 @@ interface IDepositVault is IManageableVault { uint256 minReceiveAmount, bytes32 referrerId, address tokensReceiver - ) external; + ) external returns (uint256); /** * @notice depositing proccess with mint request creating if * account fit token allowance. * Transfers token from the user. - * Transfers fee in tokenIn to feeReceiver. + * Transfers fee in tokenIn to tokensReceiver. * Creates mint request. * @param tokenIn address of tokenIn * @param amountToken amount of `tokenIn` that will be taken from user (decimals 18) @@ -200,24 +200,31 @@ interface IDepositVault is IManageableVault { ) external returns (uint256); /** - * @notice Does the same as original `depositRequest` but allows specifying a custom tokensReceiver address. + * @notice Instantly deposits `instantShare` amount of `amountMTokenIn` and creates a request for the remaining amount. * @param tokenIn address of tokenIn * @param amountToken amount of `tokenIn` that will be taken from user (decimals 18) * @param referrerId referrer id - * @param recipient address that receives the mTokens + * @param recipientRequest address that receives the mTokens for the request part + * @param instantShare % amount of `amountToken` that will be deposited instantly + * @param minReceiveAmountInstantShare min receive amount for the instant share + * @param recipientInstant address that receives the mTokens for the instant part * @return request id + * @return instantMintAmount amount of mToken that was minted instantly */ function depositRequest( address tokenIn, uint256 amountToken, bytes32 referrerId, - address recipient - ) external returns (uint256); + address recipientRequest, + uint256 instantShare, + uint256 minReceiveAmountInstantShare, + address recipientInstant + ) external returns (uint256, uint256); /** * @notice approving requests from the `requestIds` array * with the mToken rate from the request. - * Does same validation as `safeApproveRequest`. + * Validates that new mToken rate does not exceed variation tolerance * Mints mToken to request users. * Sets request flags to Processed. * @param requestIds request ids array @@ -228,16 +235,27 @@ interface IDepositVault is IManageableVault { /** * @notice approving requests from the `requestIds` array * with the current mToken rate. - * Does same validation as `safeApproveRequest`. + * Validates that new mToken rate does not exceed variation tolerance * Mints mToken to request users. * Sets request flags to Processed. * @param requestIds request ids array */ function safeBulkApproveRequest(uint256[] calldata requestIds) external; + /** + * @notice approving requests from the `requestIds` array + * with the current mToken rate. + * Validates that new mToken rate does not exceed variation tolerance + * Mints mToken to request users. + * Sets request flags to Processed. + * @param requestIds request ids array + */ + function safeBulkApproveRequestAvgRate(uint256[] calldata requestIds) + external; + /** * @notice approving requests from the `requestIds` array using the `newOutRate`. - * Does same validation as `safeApproveRequest`. + * Validates that new mToken rate does not exceed variation tolerance * Mints mToken to request users. * Sets request flags to Processed. * @param requestIds request ids array @@ -249,13 +267,17 @@ interface IDepositVault is IManageableVault { ) external; /** - * @notice approving request if inputted token rate fit price deviation percent - * Mints mToken to user. - * Sets request flag to Processed. - * @param requestId request id - * @param newOutRate mToken rate inputted by vault admin + * @notice approving requests from the `requestIds` array using the `newOutRate`. + * Validates that new mToken rate does not exceed variation tolerance + * Mints mToken to request users. + * Sets request flags to Processed. + * @param requestIds request ids array + * @param avgMTokenRate avg mToken rate inputted by vault admin */ - function safeApproveRequest(uint256 requestId, uint256 newOutRate) external; + function safeBulkApproveRequestAvgRate( + uint256[] calldata requestIds, + uint256 avgMTokenRate + ) external; /** * @notice approving request without price deviation check @@ -263,8 +285,13 @@ interface IDepositVault is IManageableVault { * Sets request flag to Processed. * @param requestId request id * @param newOutRate mToken rate inputted by vault admin + * @param isAvgRate if true, newOutRate is avg rate */ - function approveRequest(uint256 requestId, uint256 newOutRate) external; + function approveRequest( + uint256 requestId, + uint256 newOutRate, + bool isAvgRate + ) external; /** * @notice rejecting request @@ -286,4 +313,11 @@ interface IDepositVault is IManageableVault { * @param newValue new max supply cap value */ function setMaxSupplyCap(uint256 newValue) external; + + /** + * @notice sets new max amount per request + * can be called only from vault`s admin + * @param newValue new max amount per request + */ + function setMaxAmountPerRequest(uint256 newValue) external; } diff --git a/contracts/interfaces/IMToken.sol b/contracts/interfaces/IMToken.sol index ae7868b2..5ad850e7 100644 --- a/contracts/interfaces/IMToken.sol +++ b/contracts/interfaces/IMToken.sol @@ -1,29 +1,82 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity 0.8.34; -import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; /** * @title IMToken * @author RedDuck Software */ interface IMToken is IERC20Upgradeable { + /** + * @param clawbackReceiver address to which clawback tokens will be sent + */ + event ClawbackReceiverSet(address indexed clawbackReceiver); + + /** + * @notice when new limit is invalid + * @param newLimit new limit + * @param existingLimit existing limit + */ + error InvalidNewLimit(uint256 newLimit, uint256 existingLimit); + /** * @notice mints mToken token `amount` to a given `to` address. * should be called only from permissioned actor + * bypasses the timelock entirely * @param to addres to mint tokens to * @param amount amount to mint */ function mint(address to, uint256 amount) external; /** - * @notice burns mToken token `amount` to a given `to` address. + * @notice burns mToken token `amount` from a given `from` address. * should be called only from permissioned actor + * bypasses the timelock entirely * @param from addres to burn tokens from * @param amount amount to burn */ function burn(address from, uint256 amount) external; + /** + * @notice mints mToken token `amount` to a given `to` address, + * requires the timelock to pass + * should be called only from permissioned actor + * @param to address to mint tokens to + * @param amount amount to mint + */ + function mintGoverned(address to, uint256 amount) external; + + /** + * @notice burns mToken token `amount` from a given `from` address, + * bypassing blacklist checks. + * requires the timelock to pass + * should be called only from permissioned actor + * @param from address to burn tokens from + * @param amount amount to burn + */ + function burnGoverned(address from, uint256 amount) external; + + /** + * @notice claws back tokens from a given address + * @param amount amount to clawback + * @param from address to clawback tokens from + */ + function clawback(uint256 amount, address from) external; + + /** + * @notice sets the address to which clawback tokens will be sent + * @param clawbackReceiver address to which clawback tokens will be sent + */ + function setClawbackReceiver(address clawbackReceiver) external; + + /** + * @notice sets the name and symbol of the token + * @param name_ new name + * @param symbol_ new symbol + */ + function setNameSymbol(string memory name_, string memory symbol_) external; + /** * @notice updates contract`s metadata. * should be called only from permissioned actor @@ -33,14 +86,22 @@ interface IMToken is IERC20Upgradeable { function setMetadata(bytes32 key, bytes memory data) external; /** - * @notice puts mToken token on pause. - * should be called only from permissioned actor + * @notice increases mint rate limit for a given window + * @param window window duration in seconds + * @param newLimit limit amount per window */ - function pause() external; + function increaseMintRateLimit(uint256 window, uint256 newLimit) external; /** - * @notice puts mToken token on pause. - * should be called only from permissioned actor + * @notice decreases mint rate limit for a given window + * @param window window duration in seconds + * @param newLimit limit amount per window + */ + function decreaseMintRateLimit(uint256 window, uint256 newLimit) external; + + /** + * @notice removes mint rate limit config for a given window + * @param window window duration in seconds */ - function unpause() external; + function removeMintRateLimitConfig(uint256 window) external; } diff --git a/contracts/interfaces/IManageableVault.sol b/contracts/interfaces/IManageableVault.sol index ce174d21..bb8f6a75 100644 --- a/contracts/interfaces/IManageableVault.sol +++ b/contracts/interfaces/IManageableVault.sol @@ -1,18 +1,20 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity 0.8.34; -import "./IMToken.sol"; -import "./IDataFeed.sol"; +import {IMToken} from "./IMToken.sol"; +import {IDataFeed} from "./IDataFeed.sol"; /** - * @param dataFeed data feed token/USD address - * @param fee fee by token, 1% = 100 - * @param allowance token allowance (decimals 18) + * @notice Payment token config */ struct TokenConfig { + /// @notice data feed token/USD address address dataFeed; + /// @notice fee by token, 1% = 100 uint256 fee; + /// @notice token allowance (decimals 18) uint256 allowance; + /// @notice stablecoin flag bool stable; } @@ -22,17 +24,36 @@ enum RequestStatus { Canceled } -struct MTokenInitParams { +/** + * @notice Common vault init params + */ +struct CommonVaultInitParams { + /// @notice variation tolerance of mToken rates for "safe" requests approve + uint256 variationTolerance; + /// @notice minimum amount for operations in mToken + uint256 minAmount; + /// @notice fee for initial operations 1% = 100 + uint256 instantFee; + /// @notice address of the access control contract + address ac; + /// @notice address of the sanctions list contract + address sanctionsList; + /// @notice mToken address address mToken; + /// @notice mToken data feed address address mTokenDataFeed; -} -struct ReceiversInitParams { + /// @notice address to which proceeds will be sent address tokensReceiver; - address feeReceiver; -} -struct InstantInitParams { - uint256 instantFee; - uint256 instantDailyLimit; + /// @notice minimum instant fee + uint256 minInstantFee; + /// @notice maximum instant fee + uint256 maxInstantFee; + /// @notice maximum instant share value in basis points (100 = 1%) + uint256 maxInstantShare; + /// @notice max requestId that can be approved + uint256 maxApproveRequestId; + /// @notice enforce sequential request processing flag + bool sequentialRequestProcessing; } /** @@ -41,20 +62,17 @@ struct InstantInitParams { */ interface IManageableVault { /** - * @param caller function caller (msg.sender) * @param token token that was withdrawn * @param withdrawTo address to which tokens were withdrawn * @param amount `token` transfer amount */ event WithdrawToken( - address indexed caller, address indexed token, address indexed withdrawTo, uint256 amount ); /** - * @param caller function caller (msg.sender) * @param token address of token that * @param dataFeed token dataFeed address * @param fee fee 1% = 100 @@ -62,7 +80,6 @@ interface IManageableVault { * @param stable stablecoin flag */ event AddPaymentToken( - address indexed caller, address indexed token, address indexed dataFeed, uint256 fee, @@ -72,82 +89,65 @@ interface IManageableVault { /** * @param token address of token that - * @param caller function caller (msg.sender) * @param allowance new allowance */ - event ChangeTokenAllowance( - address indexed token, - address indexed caller, - uint256 allowance - ); + event ChangeTokenAllowance(address indexed token, uint256 allowance); /** * @param token address of token that - * @param caller function caller (msg.sender) * @param fee new fee */ - event ChangeTokenFee( - address indexed token, - address indexed caller, - uint256 fee - ); + event ChangeTokenFee(address indexed token, uint256 fee); /** * @param token address of token that - * @param caller function caller (msg.sender) */ - event RemovePaymentToken(address indexed token, address indexed caller); + event RemovePaymentToken(address indexed token); /** * @param account address of account - * @param caller function caller (msg.sender) + * @param enable is enabled */ - event AddWaivedFeeAccount(address indexed account, address indexed caller); + event SetWaivedFeeAccount(address indexed account, bool enable); /** - * @param account address of account - * @param caller function caller (msg.sender) + * @param newFee new operation fee value */ - event RemoveWaivedFeeAccount( - address indexed account, - address indexed caller - ); + event SetInstantFee(uint256 newFee); /** - * @param caller function caller (msg.sender) - * @param newFee new operation fee value + * @param newMinInstantFee new minimum instant fee + * @param newMaxInstantFee new maximum instant fee */ - event SetInstantFee(address indexed caller, uint256 newFee); + event SetMinMaxInstantFee( + uint256 newMinInstantFee, + uint256 newMaxInstantFee + ); /** - * @param caller function caller (msg.sender) * @param newAmount new min amount for operation */ - event SetMinAmount(address indexed caller, uint256 newAmount); + event SetMinAmount(uint256 newAmount); /** - * @param caller function caller (msg.sender) - * @param newLimit new operation daily limit + * @param newMaxInstantShare new maximum instant share value in basis points (100 = 1%) */ - event SetInstantDailyLimit(address indexed caller, uint256 newLimit); + event SetMaxInstantShare(uint256 newMaxInstantShare); /** - * @param caller function caller (msg.sender) * @param newTolerance percent of price diviation 1% = 100 */ - event SetVariationTolerance(address indexed caller, uint256 newTolerance); + event SetVariationTolerance(uint256 newTolerance); /** - * @param caller function caller (msg.sender) - * @param reciever new reciever address + * @param receiver new receiver address */ - event SetFeeReceiver(address indexed caller, address indexed reciever); + event SetTokensReceiver(address indexed receiver); /** - * @param caller function caller (msg.sender) - * @param reciever new reciever address + * @param newMaxApproveRequestId new max requestId that can be approved */ - event SetTokensReceiver(address indexed caller, address indexed reciever); + event SetMaxApproveRequestId(uint256 newMaxApproveRequestId); /** * @param user user address @@ -155,6 +155,148 @@ interface IManageableVault { */ event FreeFromMinAmount(address indexed user, bool enable); + /** + * @param enforce enforce sequential request processing flag + */ + event SetSequentialRequestProcessing(bool enforce); + + /** + * @notice Payment token is already added + * @param token token address + */ + error PaymentTokenAlreadyAdded(address token); + + /** + * @notice Payment token is not in the list + * @param token token address + */ + error PaymentTokenNotExists(address token); + + /** + * @notice Value is the same as the current one + * @param account account address + */ + error SameAddressValue(address account); + + /** + * @notice Min instant fee is greater than max instant fee + * @param minFee minimum instant fee + * @param maxFee maximum instant fee + */ + error InvalidMinMaxInstantFee(uint256 minFee, uint256 maxFee); + + /** + * @notice Amount does not match token decimals after rounding + * @param amount input amount + * @param requiredAmount amount after round-trip conversion + */ + error InvalidRounding(uint256 amount, uint256 requiredAmount); + + /** + * @notice Payment token is not supported + * @param token token address + */ + error UnknownPaymentToken(address token); + + /** + * @notice Operation amount exceeds token allowance + * @param prevAllowance current allowance + * @param amount requested amount + */ + error AllowanceExceeded(uint256 prevAllowance, uint256 amount); + + /** + * @notice Instant fee is outside min/max range + * @param instantFee current instant fee + */ + error InstantFeeOutOfBounds(uint256 instantFee); + + /** + * @notice Price change is too large + * @param difPercent actual price change percent (1% = 100) + * @param variationTolerance max allowed change percent (1% = 100) + */ + error PriceVariationExceeded( + uint256 difPercent, + uint256 variationTolerance + ); + + /** + * @notice Fee is out of allowed range + * @param fee fee value (1% = 100) + */ + error InvalidFee(uint256 fee); + + /** + * @notice Token rate is zero or invalid + * @param tokenRate token rate value + */ + error InvalidTokenRate(uint256 tokenRate); + + /** + * @notice Received amount is below minimum + * @param minReceiveAmount minimum expected amount + * @param actualReceiveAmount actual received amount + */ + error SlippageExceeded( + uint256 minReceiveAmount, + uint256 actualReceiveAmount + ); + + /** + * @notice Request id is above max approve id + * @param requestId request id + * @param maxApproveRequestId max request id that can be approved + */ + error RequestIdTooHigh(uint256 requestId, uint256 maxApproveRequestId); + + /** + * @notice New mToken rate must be greater than zero + */ + error InvalidNewMTokenRate(); + + /** + * @notice Request does not exist + * @param requestId request id + */ + error RequestNotExists(uint256 requestId); + + /** + * @notice Request has wrong status + * @param requestId request id + * @param status current request status + */ + error UnexpectedRequestStatus(uint256 requestId, RequestStatus status); + + /** + * @notice Instant share is above max allowed + * @param instantShare instant share in basis points (100 = 1%) + * @param maxInstantShare max allowed instant share + */ + error InstantShareTooHigh(uint256 instantShare, uint256 maxInstantShare); + + /** + * @notice Amount must be greater than zero + */ + error InvalidAmount(); + + /** + * @notice Amount is below minimum + * @param amount requested amount + * @param minAmount minimum allowed amount + */ + error AmountLessThanMin(uint256 amount, uint256 minAmount); + + /** + * @notice Request id is not the next expected one + * @param requestId request id + * @param nextExpectedRequestIdToProcess next request id to process + */ + error InvalidRequestSequence( + uint256 requestId, + uint256 nextExpectedRequestIdToProcess + ); + /** * @notice The mTokenDataFeed contract address. * @return The address of the mTokenDataFeed contract. @@ -167,19 +309,6 @@ interface IManageableVault { */ function mToken() external view returns (IMToken); - /** - * @notice withdraws `amount` of a given `token` from the contract. - * can be called only from permissioned actor. - * @param token token address - * @param amount token amount - * @param withdrawTo withdraw destination address - */ - function withdrawToken( - address token, - uint256 amount, - address withdrawTo - ) external; - /** * @notice adds a token to the stablecoins list. * can be called only from permissioned actor. @@ -206,7 +335,7 @@ interface IManageableVault { /** * @notice set new token allowance. - * if MAX_UINT = infinite allowance + * if type(uint256).max = infinite allowance * prev allowance rewrites by new * can be called only from permissioned actor. * @param token token address @@ -237,46 +366,63 @@ interface IManageableVault { function setMinAmount(uint256 newAmount) external; /** - * @notice adds a account to waived fee restriction. + * @notice sets a account to waived fee restriction. * can be called only from permissioned actor. * @param account user address + * @param enable is enabled */ - function addWaivedFeeAccount(address account) external; + function setWaivedFeeAccount(address account, bool enable) external; /** - * @notice removes a account from waived fee restriction. + * @notice set new receiver for tokens. * can be called only from permissioned actor. - * @param account user address + * @param receiver new token receiver address */ - function removeWaivedFeeAccount(address account) external; + function setTokensReceiver(address receiver) external; /** - * @notice set new reciever for fees. + * @notice set operation fee percent. * can be called only from permissioned actor. - * @param reciever new fee reciever address + * @param newInstantFee new instant operations fee percent 1& = 100 */ - function setFeeReceiver(address reciever) external; + function setInstantFee(uint256 newInstantFee) external; /** - * @notice set new reciever for tokens. - * can be called only from permissioned actor. - * @param reciever new token reciever address + * @notice set new minimum/maximum instant fee + * @param newMinInstantFee new minimum instant fee + * @param newMaxInstantFee new maximum instant fee */ - function setTokensReceiver(address reciever) external; + function setMinMaxInstantFee( + uint256 newMinInstantFee, + uint256 newMaxInstantFee + ) external; /** - * @notice set operation fee percent. + * @notice set operation limit configs. * can be called only from permissioned actor. - * @param newInstantFee new instant operations fee percent 1& = 100 + * @param window window duration in seconds + * @param limit limit amount per window */ - function setInstantFee(uint256 newInstantFee) external; + function setInstantLimitConfig(uint256 window, uint256 limit) external; + + /** + * @notice set maximum instant share value in basis points (100 = 1%) + * @param newMaxInstantShare new maximum instant share value in basis points (100 = 1%) + */ + function setMaxInstantShare(uint256 newMaxInstantShare) external; + + /** + * @notice sets max requestId that can be approved + * @param newMaxApproveRequestId new max requestId that can be approved + */ + function setMaxApproveRequestId(uint256 newMaxApproveRequestId) external; /** - * @notice set operation daily limit. + * @notice remove operation limit config. * can be called only from permissioned actor. - * @param newInstantDailyLimit new operation daily limit (decimals 18) + * @param window window duration in seconds */ - function setInstantDailyLimit(uint256 newInstantDailyLimit) external; + function removeInstantLimitConfig(uint256 window) external; /** * @notice frees given `user` from the minimal deposit @@ -284,4 +430,25 @@ interface IManageableVault { * @param user address of user */ function freeFromMinAmount(address user, bool enable) external; + + /** + * @notice set enforce sequential request processing flag + * @param enforce enforce sequential request processing flag + */ + function setSequentialRequestProcessing(bool enforce) external; + + /** + * @notice withdraws `amount` of a given `token` from the contract + * to the `tokensReceiver` address + * @param token token address + * @param amount token amount + */ + function withdrawToken(address token, uint256 amount) external; + + /** + * @notice check if the account is waived from fee restriction + * @param account account address + * @return true if the account is waived from fee restriction, false otherwise + */ + function waivedFeeRestriction(address account) external view returns (bool); } diff --git a/contracts/interfaces/IMidasAccessControl.sol b/contracts/interfaces/IMidasAccessControl.sol new file mode 100644 index 00000000..8d6437e9 --- /dev/null +++ b/contracts/interfaces/IMidasAccessControl.sol @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import {IAccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol"; + +interface IMidasAccessControl is IAccessControlUpgradeable { + /** + * @notice Set user facing role params + */ + struct SetUserFacingRoleParams { + /// @notice OZ role for the scope + bytes32 role; + /// @notice whether that role is user facing + bool enabled; + } + + /** + * @notice Set function access grant operator params + */ + struct SetGrantOperatorRoleParams { + /// @notice delay value + uint32 delay; + /// @notice selector of the scoped function. + bytes4 functionSelector; + /// @notice address that may call `setFunctionPermission` for this scope. + address operator; + /// @notice grant or revoke grant-operator status. + bool enabled; + } + + /** + * @notice Set function permission params + */ + struct SetPermissionRoleParams { + /// @notice address receiving or losing permission + address account; + /// @notice grant or revoke permission + bool enabled; + } + + /** + * @notice Grant role params + */ + struct GrantRoleMultParams { + /// @notice role to be granted + bytes32 role; + /// @notice account to be granted the role + address account; + /// @notice delay value + uint32 delay; + } + + /** + * @notice Revoke role params + */ + struct RevokeRoleMultParams { + /// @notice role to be revoked + bytes32 role; + /// @notice account to be revoked the role + address account; + } + + /** + * @notice Set role delay params + */ + struct SetRoleDelayParams { + /// @notice role to be set the delay for + bytes32 role; + /// @notice delay value + uint32 delay; + } + + /** + * @param role OZ role for the scope + * @param enabled whether that role is user facing + */ + event SetUserFacingRole(bytes32 indexed role, bool enabled); + + /** + * @param masterRole OZ role for the scope + * @param targetContract contract whose function is scoped. + * @param functionSelector selector of the scoped function. + * @param operator address that may call `setFunctionPermission` for this scope. + * @param enabled grant or revoke grant-operator status. + */ + event SetGrantOperatorRole( + bytes32 indexed masterRole, + address indexed targetContract, + address indexed operator, + bytes4 functionSelector, + bool enabled + ); + + /** + * @param masterRole OZ role for the scope + * @param targetContract contract whose function is scoped. + * @param account address receiving or losing permission + * @param functionSelector selector of the scoped function. + * @param enabled grant or revoke + */ + event SetPermissionRole( + bytes32 indexed masterRole, + address indexed targetContract, + address indexed account, + bytes4 functionSelector, + bool enabled + ); + + /** + * @param defaultDelay new default delay + */ + event SetDefaultDelay(uint32 defaultDelay); + + /** + * @param params array of SetRoleDelayParams + */ + event SetRoleDelays(SetRoleDelayParams[] params); + + /** + * @param role role id + * @param delay delay value + */ + event SetRoleDelay(bytes32 role, uint32 delay); + + /** + * @notice when the array is empty + */ + error EmptyArray(); + + /** + * @notice when the arrays have different lengths + * @param length1 length of the first array + * @param length2 length of the second array + */ + error MismatchArrays(uint256 length1, uint256 length2); + + /** + * @notice error when the function is forbidden + */ + error Forbidden(); + + /** + * @notice when the role is being revoked from the self + * @param role role to be revoked + * @param account account to be revoked + */ + error CannotRevokeFromSelf(bytes32 role, address account); + + /** + * @notice when the delay is already set + */ + error DelayIsAlreadySet(); + + /** + * @notice when the role admin mismatch + * @param role role to be revoked + * @param adminRole admin role + */ + error RoleAdminMismatch(bytes32 role, bytes32 adminRole); + + /** + * @notice Enable or disable which OZ role may administer function-access scopes for that role. + * @dev Only `DEFAULT_ADMIN_ROLE` can call this function. + * Prevents unrelated role admins from spamming access mappings. + * @param params array of SetUserFacingRoleParams + */ + function setUserFacingRoleMult(SetUserFacingRoleParams[] calldata params) + external; + + /** + * @notice Add or remove a grant operator for a specific contract function scope. + * @dev `targetContract` must implement `IMidasAccessControlManaged` interface; + * Caller must hold `contractAdminRole` of a target contract; + * @param targetContract scoped contract + * @param params array of SetGrantOperatorRoleParams + */ + function setGrantOperatorRoleMult( + address targetContract, + SetGrantOperatorRoleParams[] calldata params + ) external; + + /** + * @notice Grant or revoke function access for an account + * @dev caller must be a grant operator for the scope or have the master role + * target contract must implement `IMidasAccessControlManaged` interface; + * @param targetContract scoped contract + * @param functionSelector scoped function + * @param delay delay value + * @param params array of SetPermissionRoleParams + */ + function setPermissionRoleMult( + address targetContract, + bytes4 functionSelector, + uint32 delay, + SetPermissionRoleParams[] calldata params + ) external; + + /** + * @notice Grant or revoke function access for an account + * @dev caller must be a grant operator for the scope or have the master role + * @param masterRole OZ role for the scope + * @param targetContract scoped contract + * @param functionSelector scoped function + * @param delay delay value + * @param params array of SetPermissionRoleParams + */ + function setPermissionRoleMult( + bytes32 masterRole, + address targetContract, + bytes4 functionSelector, + uint32 delay, + SetPermissionRoleParams[] calldata params + ) external; + + /** + * @notice Grant a role to an account with a delay + * @param role role id + * @param account account to grant the role to + * @param delay delay value + */ + function grantRole( + bytes32 role, + address account, + uint32 delay + ) external; + + /** + * @notice grant multiple roles to multiple users in one transaction + * @param params array of GrantRoleMultParams + */ + function grantRoleMult(GrantRoleMultParams[] calldata params) external; + + /** + * @notice revoke multiple roles from multiple users in one transaction + * @param params array of RevokeRoleMultParams + */ + function revokeRoleMult(RevokeRoleMultParams[] calldata params) external; + + /** + * @notice Sets the default delay + * @param _defaultDelay default delay in seconds + */ + function setDefaultDelay(uint32 _defaultDelay) external; + + /** + * @notice Sets timelock delay per role + * @param params array of SetRoleDelayParams + */ + function setRoleDelayMult(SetRoleDelayParams[] calldata params) external; + + /** + * @notice set the admin role for a specific role + * @dev can be called only by the address that holds `DEFAULT_ADMIN_ROLE` + * @param role the role to set the admin role for + * @param newAdminRole the new admin role + */ + function setRoleAdmin(bytes32 role, bytes32 newAdminRole) external; + + /** + * @notice Whether `role` is user facing. + * @param role OZ role for the scope + * @return enabled whether `role` is user facing + */ + function isUserFacingRole(bytes32 role) external view returns (bool); + + /** + * @notice Whether `operator` may call `setFunctionPermission` for the function scope + * @param masterRole OZ role for the scope + * @param targetContract scoped contract + * @param functionSelector scoped function + * @param operator address checked for grant-operator status + * @return allowed whether `operator` is a grant operator for the scope + */ + function isFunctionAccessGrantOperator( + bytes32 masterRole, + address targetContract, + bytes4 functionSelector, + address operator + ) external view returns (bool); + + /** + * @notice Whether `operator` may call `setFunctionPermission` for the function scope + * @param key operator permission key + * @param operator address checked for grant-operator status + * @return allowed whether `operator` is a grant operator for the scope + */ + function isFunctionAccessGrantOperator(bytes32 key, address operator) + external + view + returns (bool); + + /** + * @notice Whether `account` may call the scoped function on `targetContract`. + * @param masterRole OZ role for the scope + * @param targetContract scoped contract + * @param functionSelector scoped function + * @param account address checked for permissio. + * @return allowed whether `account` has function access for the scope + */ + function hasFunctionPermission( + bytes32 masterRole, + address targetContract, + bytes4 functionSelector, + address account + ) external view returns (bool); + + /** + * @notice Whether `account` has function access for the scope. + * @param key the base key for function permission mappings + * @param account address checked for permission + * @return allowed whether `account` has function access for the scope + */ + function hasFunctionPermission(bytes32 key, address account) + external + view + returns (bool); + + /** + * @notice calculates the base key for function permission mappings + * @param masterRole OZ role + * @param targetContract scoped contract + * @param functionSelector scoped function of a `targetContract` + * @return key the base key for function permission mappings + */ + function permissionRoleKey( + bytes32 masterRole, + address targetContract, + bytes4 functionSelector + ) external pure returns (bytes32); + + /** + * @notice calculates the base key for function permission mappings + * @param masterRole OZ role + * @param targetContract scoped contract + * @param functionSelector scoped function of a `targetContract` + * @return key the base key for function permission mappings + */ + function grantOperatorRoleKey( + bytes32 masterRole, + address targetContract, + bytes4 functionSelector + ) external pure returns (bytes32); + + /** + * @notice Returns timelock delay for a role + * @param role role id + * @param overrideDelay override delay for the invocation + * @return delay effective delay in seconds + * @return isDefault true if role uses default delay + */ + function getRoleTimelockDelay(bytes32 role, uint32 overrideDelay) + external + view + returns (uint32 delay, bool isDefault); + + /** + * @notice Default timelock delay when role delay is not set + * @return delay delay in seconds + */ + function defaultDelay() external view returns (uint32 delay); + + /** + * @notice address of the timelock manager + * @return timelockManager address of the timelock manager + */ + function timelockManager() external view returns (address); + + /** + * @notice address of the pause manager + * @return pauseManager address of the pause manager + */ + function pauseManager() external view returns (address); +} diff --git a/contracts/interfaces/IMidasAccessControlManaged.sol b/contracts/interfaces/IMidasAccessControlManaged.sol new file mode 100644 index 00000000..b76f926d --- /dev/null +++ b/contracts/interfaces/IMidasAccessControlManaged.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +/** + * @title IMidasAccessControlManaged + * @notice Interface for contracts that are managed by the MidasAccessControl + * @author RedDuck Software + */ +interface IMidasAccessControlManaged { + /** + * @notice returns the role that can pause the contract + * @return role role descriptor + */ + function contractAdminRole() external view returns (bytes32); +} diff --git a/contracts/interfaces/IMidasPauseManager.sol b/contracts/interfaces/IMidasPauseManager.sol new file mode 100644 index 00000000..04aa9623 --- /dev/null +++ b/contracts/interfaces/IMidasPauseManager.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +/** + * @title IMidasPauseManager + * @notice Interface for the MidasPauseManager + * @author RedDuck Software + */ +interface IMidasPauseManager { + /** + * @param contractAddr contract address + * @param fn function id + * @param isPaused paused status + */ + event FnPauseStatusChange( + address indexed contractAddr, + bytes4 indexed fn, + bool isPaused + ); + + /** + * @param contractAddr contract address + * @param isPaused paused status + */ + event ContractPauseStatusChange( + address indexed contractAddr, + bool isPaused + ); + + /** + * @param isPaused paused status + */ + event GlobalPauseStatusChange(bool isPaused); + + /** + * @param pauseDelay pause delay + */ + event SetPauseDelay(uint32 pauseDelay); + + /** + * @param unpauseDelay unpause delay + */ + event SetUnpauseDelay(uint32 unpauseDelay); + + /** + * @notice sets the pause delay + * @dev can be called only by the pause manager admin or function admin + * @param _pauseDelay pause delay + */ + function setPauseDelay(uint32 _pauseDelay) external; + + /** + * @notice sets the unpause delay + * @dev can be called only by the pause manager admin or function admin + * @param _unpauseDelay unpause delay + */ + function setUnpauseDelay(uint32 _unpauseDelay) external; + + /** + * @notice pauses the protocol + * @dev can be called only by the pause manager admin + */ + function globalPause() external; + + /** + * @notice unpauses the protocol + * @dev can be called only by the pause manager admin + */ + function globalUnpause() external; + + /** + * @notice pauses an array of contracts + * @dev can be called only by the pause manager admin or function admin + * @param contractAddrs array of contract addresses + */ + function bulkPauseContract(address[] calldata contractAddrs) external; + + /** + * @notice unpauses an array of contracts + * @dev can be called only by the pause manager admin or function admin + * @param contractAddrs array of contract addresses + */ + function bulkUnpauseContract(address[] calldata contractAddrs) external; + + /** + * @notice pauses functions on an array of contracts + * @dev can be called only by the pause manager admin or function admin + * @param contractAddrs array of contract addresses + * @param selectors function ids to pause on the contracts + */ + function bulkPauseContractFn( + address[] calldata contractAddrs, + bytes4[] calldata selectors + ) external; + + /** + * @notice unpauses functions on an array of contracts + * @dev can be called only by the pause manager admin or function admin + * @param contractAddrs array of contract addresses + * @param selectors function ids to unpause on the contracts + */ + function bulkUnpauseContractFn( + address[] calldata contractAddrs, + bytes4[] calldata selectors + ) external; + + /** + * @notice pauses a contract + * @dev can be called only by admin of a contract or function admin that + * is managed by the admin of the contract + * @param contractAddr address of the contract + */ + function contractAdminPause(address contractAddr) external; + + /** + * @notice unpauses a contract + * @dev can be called only by admin of a contract or function admin that + * is managed by the admin of the contract + * @param contractAddr address of the contract + */ + function contractAdminUnpause(address contractAddr) external; + + /** + * @notice checks if function of a contract is paused + * @param contractAddr contract address + * @param selector function id + * @return paused true if the function is paused + */ + function isFunctionPaused(address contractAddr, bytes4 selector) + external + view + returns (bool); + + /** + * @notice checks if function or contract or protocol is paused + * @param contractAddr contract address + * @return paused true if paused + */ + function isPaused(address contractAddr, bytes4 selector) + external + view + returns (bool); + + /** + * @notice returns the admin role for the pause manager + * @return role admin role + */ + function pauseAdminRole() external view returns (bytes32); + + /** + * @notice returns the pause delay + * @return pause delay + */ + function pauseDelay() external view returns (uint32); + + /** + * @notice returns the unpause delay + * @return unpause delay + */ + function unpauseDelay() external view returns (uint32); +} diff --git a/contracts/interfaces/IMidasTimelockManager.sol b/contracts/interfaces/IMidasTimelockManager.sol new file mode 100644 index 00000000..dd8b8d98 --- /dev/null +++ b/contracts/interfaces/IMidasTimelockManager.sol @@ -0,0 +1,442 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +/** + * @notice Timelock operation status + * @dev Computed status may differ from stored status (expiry, dispute period). + */ +enum TimelockOperationStatus { + NotExist, + Pending, + Paused, + ApprovedExecution, + ReadyToExecute, + ReadyToAbort, + Expired, + Aborted, + Executed +} + +/** + * @notice Operation details returned by `getOperationDetails` + */ +struct GetOperationStatusResult { + /// @notice current status + TimelockOperationStatus status; + /// @notice block timestamp when operation was scheduled + uint32 createdAt; + /// @notice block timestamp when execution was approved by council + uint32 executionApprovedAt; + /// @notice pause reason code set by pauser + uint8 pauseReasonCode; + /// @notice security council version at pause time + uint256 councilVersion; + /// @notice address that scheduled the operation + address operationProposer; + /// @notice address that paused the operation + address pauser; + /// @notice hash of target, value and data + bytes32 dataHash; + /// @notice number of council votes for execution + uint8 votesForExecution; + /// @notice number of council votes for veto + uint8 votesForVeto; + /// @notice true if operation updates security council + bool isSetCouncilOperation; +} + +/** + * @title IMidasTimelockManager + * @notice Interface for the MidasTimelockManager + * @author RedDuck Software + */ +interface IMidasTimelockManager { + /** + * @notice Parameters for scheduling a timelock operation + * @param target target contract + * @param data calldata + */ + struct ScheduleTimelockOperationParams { + /// @notice target contract + address target; + /// @notice calldata + bytes data; + } + + /** + * @param maxPendingOperationsPerProposer new limit + */ + event SetMaxPendingOperationsPerProposer( + uint256 maxPendingOperationsPerProposer + ); + + /** + * @param version new security council version + * @param members council member addresses + */ + event SetSecurityCouncil(uint256 indexed version, address[] members); + + /** + * @param caller operation proposer + * @param operationId scheduled operation id + */ + event ScheduleTimelockOperation( + address indexed caller, + bytes32 indexed operationId + ); + + /** + * @param caller pauser address + * @param operationId paused operation id + * @param pauseReasonCode pause reason code + * @param councilVersion security council version at pause + */ + event PauseTimelockOperation( + address indexed caller, + bytes32 indexed operationId, + uint8 indexed pauseReasonCode, + uint256 councilVersion + ); + + /** + * @param caller executor address + * @param operationId executed operation id + */ + event ExecuteTimelockOperation( + address indexed caller, + bytes32 indexed operationId + ); + + /** + * @param caller council member address + * @param operationId operation id + * @param votedForExecution true for execution vote, false for veto vote + */ + event PausedProposalVoteCast( + address indexed caller, + bytes32 indexed operationId, + bool indexed votedForExecution + ); + + /** + * @param caller address that aborted the operation + * @param operationId aborted operation id + * @param status status before abort + */ + event AbortTimelockOperation( + address indexed caller, + bytes32 indexed operationId, + TimelockOperationStatus status + ); + + /** + * @notice Preflight call succeeded with role info + * @param role role used for the call + * @param overrideDelay override delay for the invocation + * @param roleIsFunctionOperator true if role is function operator + * @param validateFunctionRole true if function role should be validated + */ + error RolePreflightSucceeded( + bytes32 role, + uint32 overrideDelay, + bool roleIsFunctionOperator, + bool validateFunctionRole + ); + + /** + * @notice Timelock address is already set + */ + error TimelockAlreadySet(); + + /** + * @notice Operation status is not valid for this action + * @param actualStatus current operation status + */ + error UnexpectedOperationStatus(TimelockOperationStatus actualStatus); + + /** + * @notice Operation is not in the pending set + */ + error OperationNotPending(); + + /** + * @notice Operation is already pending + */ + error OperationAlreadyPending(); + + /** + * @notice Timelock delay has not passed yet + */ + error TimelockOperationNotReady(); + + /** + * @notice Caller is not a security council member for this operation + */ + error NotInSecurityCouncil(); + + /** + * @notice Council member already voted + */ + error AlreadyVoted(); + + /** + * @notice Role has no timelock delay configured + */ + error NoTimelockDelayForRole(); + + /** + * @notice Proposer has too many pending operations + */ + error TooManyPendingOperations(); + + /** + * @notice Pending set-council operation already exists + */ + error PendingSetCouncilOperationExists(); + + /** + * @notice Security council size is out of allowed range + */ + error InvalidSecurityCouncilMembersLength(); + + /** + * @notice Max pending operations value is invalid + */ + error InvalidMaxPendingOperationsPerProposer(); + + /** + * @notice Target call should have reverted on preflight + */ + error PreflightCallUnexpectedSuccess(); + + /** + * @notice Preflight revert data is invalid + * @param err revert bytes + */ + error InvalidPreflightError(bytes err); + + /** + * @notice Sets max pending operations per proposer + * @param _maxPendingOperationsPerProposer new limit + */ + function setMaxPendingOperationsPerProposer( + uint256 _maxPendingOperationsPerProposer + ) external; + + /** + * @notice Sets a new security council version + * @param members council member addresses + */ + function setSecurityCouncil(address[] calldata members) external; + + /** + * @notice Schedules multiple timelock operations + * @param params array of schedule timelock operation parameters + */ + function bulkScheduleTimelockOperation( + ScheduleTimelockOperationParams[] calldata params + ) external; + + /** + * @notice Schedules one timelock operation + * @param params schedule timelock operation parameters + */ + function scheduleTimelockOperation( + ScheduleTimelockOperationParams calldata params + ) external; + + /** + * @notice Executes a scheduled timelock operation + * @param target target contract + * @param data operation data + */ + function executeTimelockOperation(address target, bytes calldata data) + external; + + /** + * @notice Pauses a pending operation + * @param operationId operation id + * @param pauseReasonCode reason code set by pauser + */ + function pauseOperation(bytes32 operationId, uint8 pauseReasonCode) + external; + + /** + * @notice Security council votes to abort the operation + * @dev can vote even if member is already voted for execution + * @param operationId operation id + */ + function voteForVeto(bytes32 operationId) external; + + /** + * @notice Security council votes to allow execution + * @dev cannot vote if member is already voted for veto + * @param operationId operation id + */ + function voteForExecution(bytes32 operationId) external; + + /** + * @notice Aborts operation after veto quorum or expiry + * @param operationId operation id + */ + function abortOperation(bytes32 operationId) external; + + /** + * @notice Returns original proposer for a pending operation + * @param target target contract + * @param data operation data + * @return proposer address + */ + function getOriginalProposer(address target, bytes calldata data) + external + view + returns (address); + + /** + * @notice Votes needed for council quorum at a version + * @param version security council version + * @return quorum required votes + */ + function councilQuorum(uint256 version) + external + view + returns (uint8 quorum); + + /** + * @notice Whether a council member voted on an operation + * @param operationId operation id + * @param councilMember member address + * @return votedForExecution true if voted for execution + * @return votedForVeto true if voted for veto + */ + function getCouncilMemberVoteStatus( + bytes32 operationId, + address councilMember + ) external view returns (bool votedForExecution, bool votedForVeto); + + /** + * @notice Returns all pending operation ids + * @return operationIds pending operation ids + */ + function getPendingOperations() + external + view + returns (bytes32[] memory operationIds); + + /** + * @notice Returns full operation details + * @param operationId operation id + * @return result operation details + */ + function getOperationDetails(bytes32 operationId) + external + view + returns (GetOperationStatusResult memory result); + + /** + * @notice Returns operation status (with expiry/dispute rules applied) + * @param operationId operation id + * @return status current status + */ + function getOperationStatus(bytes32 operationId) + external + view + returns (TimelockOperationStatus status); + + /** + * @notice Returns stored operation status without adjustments + * @param operationId operation id + * @return status stored status + */ + function getOperationStatusRaw(bytes32 operationId) + external + view + returns (TimelockOperationStatus status); + + /** + * @notice Returns security council members for a version + * @param version security council version + * @return members member addresses + */ + function getSecurityCouncilMembers(uint256 version) + external + view + returns (address[] memory members); + + /** + * @notice Returns operation id for target and data + * @param target target contract + * @param data operation data + * @return operationId operation id + */ + function getOperationId(address target, bytes calldata data) + external + view + returns (bytes32 operationId); + + /** + * @dev gets the target role for a given operation + * @param target target contract + * @param data operation data + * @param proposer operation proposer address + * @return role target role + * @return overrideDelay override delay for the invocation + */ + function getTargetRole( + address target, + bytes calldata data, + address proposer + ) external view returns (bytes32 role, uint32 overrideDelay); + + /** + * @notice Checks if an account is in the security council for a given version + * @param version security council version + * @param account account to check + * @return true if the account is in the security council + */ + function isInSecurityCouncil(uint256 version, address account) + external + view + returns (bool); + + /** + * @notice Timelock controller address + * @return timelockAddress timelock controller + */ + function timelock() external view returns (address timelockAddress); + + /** + * @notice Max pending operations per proposer + * @return value current limit + */ + function maxPendingOperationsPerProposer() external view returns (uint256); + + /** + * @notice Current security council version + * @return version council version + */ + function securityCouncilVersion() external view returns (uint256); + + /** + * @notice Data hash index used for operation id salt + * @param dataHash operation data hash + * @return index current index for this data hash + */ + function dataHashIndexes(bytes32 dataHash) external view returns (uint256); + + /** + * @notice Pending operations count for a proposer + * @param proposer proposer address + * @return count pending count + */ + function proposerPendingOperationsCount(address proposer) + external + view + returns (uint256); + + /** + * @notice Pending set-security-council operation id, if any + * @return operationId operation id or zero + */ + function pendingSetCouncilOperationId() external view returns (bytes32); +} diff --git a/contracts/interfaces/IRedemptionVault.sol b/contracts/interfaces/IRedemptionVault.sol index 2c21f659..2215910d 100644 --- a/contracts/interfaces/IRedemptionVault.sol +++ b/contracts/interfaces/IRedemptionVault.sol @@ -1,30 +1,64 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity 0.8.34; -import "./IManageableVault.sol"; +import {IManageableVault, RequestStatus} from "./IManageableVault.sol"; /** * @notice Redeem request scruct - * @param sender user address who create - * @param tokenOut tokenOut address - * @param status request status - * @param amountMToken amount mToken - * @param mTokenRate rate of mToken at request creation time - * @param tokenOutRate rate of tokenOut at request creation time */ struct Request { - address sender; + /// @notice user address which will receive the mTokens + address recipient; + /// @notice tokenOut address address tokenOut; + /// @notice request status RequestStatus status; + /// @notice fixed fee percent that was calculated at request creation time + uint256 feePercent; + /// @notice amount of mToken uint256 amountMToken; + /// @notice rate of mToken at request creation time uint256 mTokenRate; + /// @notice rate of tokenOut at request creation time uint256 tokenOutRate; + /// @notice amount of mToken that was redeemed instantly + uint256 amountMTokenInstant; + /// @notice approved mToken rate + uint256 approvedMTokenRate; + /// @notice amount of tokenOut that was redeeme + uint256 amountTokenOut; } -struct FiatRedeptionInitParams { - uint256 fiatAdditionalFee; - uint256 fiatFlatFee; - uint256 minFiatRedeemAmount; +/** + * @notice Redemption vault init params + */ +struct RedemptionVaultInitParams { + /// @notice address of request redeemer + address requestRedeemer; + /// @notice address of loan liquidity provider + address loanLp; + /// @notice address of loan repayment address + address loanRepaymentAddress; + /// @notice address of loan swapper vault + address loanSwapperVault; + /// @notice loan APR value in basis points (100 = 1%) + uint256 loanApr; +} + +/** + * @notice Liquidity provider loan request struct + */ +struct LiquidityProviderLoanRequest { + /// @notice tokenOut address + address tokenOut; + /// @notice amount of tokenOut + uint256 amountTokenOut; + /// @notice amount of tokenOut fee + uint256 amountFee; + /// @notice timestamp of the request creation + uint256 createdAt; + /// @notice status of the loan + RequestStatus status; } /** @@ -35,123 +69,149 @@ interface IRedemptionVault is IManageableVault { /** * @param user function caller (msg.sender) * @param tokenOut address of tokenOut - * @param amount amount of mToken - * @param feeAmount fee amount in mToken + * @param recipient recipient address + * @param amountMToken amount of mToken + * @param feeAmount fee amount in tokenOut * @param amountTokenOut amount of tokenOut + * @param mTokenRate mToken rate + * @param tokenOutRate tokenOut rate */ event RedeemInstant( address indexed user, address indexed tokenOut, - uint256 amount, + address recipient, + uint256 amountMToken, uint256 feeAmount, - uint256 amountTokenOut + uint256 amountTokenOut, + uint256 mTokenRate, + uint256 tokenOutRate ); /** + * @param requestId request id * @param user function caller (msg.sender) * @param tokenOut address of tokenOut - * @param recipient address that receives tokens - * @param amount amount of mToken - * @param feeAmount fee amount in mToken - * @param amountTokenOut amount of tokenOut + * @param recipient recipient address + * @param amountMToken amount of mToken + * @param amountMTokenInstant amount of mToken that was redeemed instantly + * @param feePercent fee percent + * @param mTokenRate mToken rate + * @param tokenOutRate tokenOut rate */ - event RedeemInstantWithCustomRecipient( + event RedeemRequest( + uint256 indexed requestId, address indexed user, address indexed tokenOut, address recipient, - uint256 amount, - uint256 feeAmount, - uint256 amountTokenOut + uint256 amountMToken, + uint256 amountMTokenInstant, + uint256 feePercent, + uint256 mTokenRate, + uint256 tokenOutRate ); /** - * @param requestId request id - * @param user function caller (msg.sender) - * @param tokenOut address of tokenOut - * @param amountMTokenIn amount of mToken - * @param feeAmount fee amount in mToken + * @param loanId loan id + * @param tokenOut tokenOut address + * @param amountTokenOut amount of tokenOut + * @param amountFee fee amount in payment token + * @param mTokenRate mToken rate + * @param tokenOutRate tokenOut rate */ - event RedeemRequest( - uint256 indexed requestId, - address indexed user, + event CreateLiquidityProviderLoanRequest( + uint256 indexed loanId, address indexed tokenOut, - uint256 amountMTokenIn, - uint256 feeAmount + uint256 amountTokenOut, + uint256 amountFee, + uint256 mTokenRate, + uint256 tokenOutRate ); /** - * @param requestId request id - * @param user function caller (msg.sender) - * @param tokenOut address of tokenOut - * @param recipient address that receives tokens - * @param amountMTokenIn amount of mToken - * @param feeAmount fee amount in mToken + * @param requestId mint request id + * @param newMTokenRate new mToken rate + * @param isSafe if true, approval is safe + * @param isAvgRate if true, newMTokenRate is avg rate */ - event RedeemRequestWithCustomRecipient( + event ApproveRequest( uint256 indexed requestId, - address indexed user, - address indexed tokenOut, - address recipient, - uint256 amountMTokenIn, - uint256 feeAmount + uint256 newMTokenRate, + bool isSafe, + bool isAvgRate ); /** * @param requestId mint request id - * @param newMTokenRate net mToken rate */ - event ApproveRequest(uint256 indexed requestId, uint256 newMTokenRate); + event RejectRequest(uint256 indexed requestId); /** - * @param requestId mint request id - * @param newMTokenRate net mToken rate + * @param redeemer new address of request redeemer */ - event SafeApproveRequest(uint256 indexed requestId, uint256 newMTokenRate); + event SetRequestRedeemer(address indexed redeemer); /** - * @param requestId mint request id - * @param user address of user + * @param newLoanLp new address of loan liquidity provider */ - event RejectRequest(uint256 indexed requestId, address indexed user); + event SetLoanLp(address indexed newLoanLp); /** - * @param caller function caller (msg.sender) - * @param newMinAmount new min amount for fiat requests + * @param newLoanRepaymentAddress new address of loan repayment address */ - event SetMinFiatRedeemAmount(address indexed caller, uint256 newMinAmount); + event SetLoanRepaymentAddress(address newLoanRepaymentAddress); /** - * @param caller function caller (msg.sender) - * @param feeInMToken fee amount in mToken + * @param newLoanSwapperVault new address of loan swapper vault */ - event SetFiatFlatFee(address indexed caller, uint256 feeInMToken); + event SetLoanSwapperVault(address newLoanSwapperVault); /** - * @param caller function caller (msg.sender) - * @param newfee new fiat fee percent 1% = 100 + * @param newLoanApr new loan APR value in basis points (100 = 1%) */ - event SetFiatAdditionalFee(address indexed caller, uint256 newfee); + event SetLoanApr(uint256 newLoanApr); /** - * @param caller function caller (msg.sender) - * @param redeemer new address of request redeemer + * @param newLoanLpFirst new flag to determine if the loan LP liquidity should be used first */ - event SetRequestRedeemer(address indexed caller, address redeemer); + event SetPreferLoanLiquidity(bool newLoanLpFirst); + + /** + * @param requestId request id + * @param amountFee amount of fee in tokenOut + */ + event RepayLpLoanRequest(uint256 indexed requestId, uint256 amountFee); + + /** + * @param requestId request id + */ + event CancelLpLoanRequest(uint256 indexed requestId); + + /** + * @notice when fee exceeds amount + * @param fee fee + * @param amount amount + */ + error FeeExceedsAmount(uint256 fee, uint256 amount); + + /** + * @notice when not self call + */ + error NotSelfCall(); /** * @notice redeem mToken to tokenOut if daily limit and allowance not exceeded * Burns mToken from the user. - * Transfers fee in mToken to feeReceiver * Transfers tokenOut to user. * @param tokenOut stable coin token address to redeem to * @param amountMTokenIn amount of mToken to redeem (decimals 18) * @param minReceiveAmount minimum expected amount of tokenOut to receive (decimals 18) + * @return amountTokenOut amount of tokenOut that was received in original decimals */ function redeemInstant( address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount - ) external; + ) external returns (uint256); /** * @notice Does the same as original `redeemInstant` but allows specifying a custom tokensReceiver address. @@ -159,18 +219,18 @@ interface IRedemptionVault is IManageableVault { * @param amountMTokenIn amount of mToken to redeem (decimals 18) * @param minReceiveAmount minimum expected amount of tokenOut to receive (decimals 18) * @param recipient address that receives tokens + * @return amountTokenOut amount of tokenOut that was received in original decimals */ function redeemInstant( address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount, address recipient - ) external; + ) external returns (uint256); /** - * @notice creating redeem request if tokenOut not fiat + * @notice creating redeem request * Transfers amount in mToken to contract - * Transfers fee in mToken to feeReceiver * @param tokenOut stable coin token address to redeem to * @param amountMTokenIn amount of mToken to redeem (decimals 18) * @return request id @@ -180,34 +240,30 @@ interface IRedemptionVault is IManageableVault { returns (uint256); /** - * @notice Does the same as original `redeemRequest` but allows specifying a custom tokensReceiver address. + * @notice Instantly redeems `instantShare` amount of `amountMTokenIn` and creates a request for the remaining amount. * @param tokenOut stable coin token address to redeem to * @param amountMTokenIn amount of mToken to redeem (decimals 18) - * @param recipient address that receives tokens + * @param recipientRequest address that receives tokens for the request part + * @param instantShare % amount of `amountMTokenIn` that will be redeemed instantly + * @param minReceiveAmountInstantShare min receive amount for the instant share + * @param recipientInstant address that receives tokens for the instant part * @return request id + * @return instantReceivedAmount amount of tokenOut that was received instantly in original decimals */ function redeemRequest( address tokenOut, uint256 amountMTokenIn, - address recipient - ) external returns (uint256); - - /** - * @notice creating redeem request if tokenOut is fiat - * Transfers amount in mToken to contract - * Transfers fee in mToken to feeReceiver - * @param amountMTokenIn amount of mToken to redeem (decimals 18) - * @return request id - */ - function redeemFiatRequest(uint256 amountMTokenIn) - external - returns (uint256); + address recipientRequest, + uint256 instantShare, + uint256 minReceiveAmountInstantShare, + address recipientInstant + ) external returns (uint256, uint256); /** * @notice approving requests from the `requestIds` array with the mToken rate * from the request. WONT fail even if there is not enough liquidity * to process all requests. - * Does same validation as `safeApproveRequest`. + * Validates that new mToken rate does not exceed variation tolerance * Transfers tokenOut to users * Sets request flags to Processed. * @param requestIds request ids array @@ -219,17 +275,29 @@ interface IRedemptionVault is IManageableVault { * @notice approving requests from the `requestIds` array with the * current mToken rate. WONT fail even if there is not enough liquidity * to process all requests. - * Does same validation as `safeApproveRequest`. + * Validates that new mToken rate does not exceed variation tolerance * Transfers tokenOut to users * Sets request flags to Processed. * @param requestIds request ids array */ function safeBulkApproveRequest(uint256[] calldata requestIds) external; + /** + * @notice approving requests from the `requestIds` array with the + * current mToken rate as avg rate. WONT fail even if there is not enough liquidity + * to process all requests. + * Validates that new mToken rate does not exceed variation tolerance + * Transfers tokenOut to users + * Sets request flags to Processed. + * @param requestIds request ids array + */ + function safeBulkApproveRequestAvgRate(uint256[] calldata requestIds) + external; + /** * @notice approving requests from the `requestIds` array using the `newMTokenRate`. * WONT fail even if there is not enough liquidity to process all requests. - * Does same validation as `safeApproveRequest`. + * Validates that new mToken rate does not exceed variation tolerance * Transfers tokenOut to user * Sets request flags to Processed. * @param requestIds request ids array @@ -241,25 +309,33 @@ interface IRedemptionVault is IManageableVault { ) external; /** - * @notice approving redeem request if not exceed tokenOut allowance - * Burns amount mToken from contract + * @notice approving requests from the `requestIds` array using the `avgMTokenRate`. + * WONT fail even if there is not enough liquidity to process all requests. + * Validates that new mToken rate does not exceed variation tolerance * Transfers tokenOut to user - * Sets flag Processed - * @param requestId request id - * @param newMTokenRate new mToken rate inputted by vault admin + * Sets request flags to Processed. + * @param requestIds request ids array + * @param avgMTokenRate avg mToken rate inputted by vault admin */ - function approveRequest(uint256 requestId, uint256 newMTokenRate) external; + function safeBulkApproveRequestAvgRate( + uint256[] calldata requestIds, + uint256 avgMTokenRate + ) external; /** - * @notice approving request if inputted token rate fit price diviation percent + * @notice approving redeem request if not exceed tokenOut allowance * Burns amount mToken from contract * Transfers tokenOut to user * Sets flag Processed * @param requestId request id * @param newMTokenRate new mToken rate inputted by vault admin + * @param isAvgRate if true, newMTokenRate is avg rate */ - function safeApproveRequest(uint256 requestId, uint256 newMTokenRate) - external; + function approveRequest( + uint256 requestId, + uint256 newMTokenRate, + bool isAvgRate + ) external; /** * @notice rejecting request @@ -269,26 +345,53 @@ interface IRedemptionVault is IManageableVault { function rejectRequest(uint256 requestId) external; /** - * @notice set new min amount for fiat requests - * @param newValue new min amount - */ - function setMinFiatRedeemAmount(uint256 newValue) external; - - /** - * @notice set fee amount in mToken for fiat requests - * @param feeInMToken fee amount in mToken + * @notice repaying loan requests from the `requestIds` array + * Transfers tokenOut to loan repayment address + * Sets request flags to Processed. + * @param requestIds request ids array */ - function setFiatFlatFee(uint256 feeInMToken) external; + function bulkRepayLpLoanRequest(uint256[] calldata requestIds) external; /** - * @notice set new fee percent for fiat requests - * @param newFee new fee percent 1% = 100 + * @notice canceling loan request + * Sets request flags to Canceled. + * @param requestId request id */ - function setFiatAdditionalFee(uint256 newFee) external; + function cancelLpLoanRequest(uint256 requestId) external; /** * @notice set address which is designated for standard redemptions, allowing tokens to be pulled from this address * @param redeemer new address of request redeemer */ function setRequestRedeemer(address redeemer) external; + + /** + * @notice set address of loan liquidity provider + * @param newLoanLp new address of loan liquidity provider + */ + function setLoanLp(address newLoanLp) external; + + /** + * @notice set address of loan repayment address + * @param newLoanRepaymentAddress new address of loan repayment address + */ + function setLoanRepaymentAddress(address newLoanRepaymentAddress) external; + + /** + * @notice set address of loan swapper vault + * @param newLoanSwapperVault new address of loan swapper vault + */ + function setLoanSwapperVault(address newLoanSwapperVault) external; + + /** + * @notice set loan APR value in basis points (100 = 1%) + * @param newLoanApr new loan APR value in basis points (100 = 1%) + */ + function setLoanApr(uint256 newLoanApr) external; + + /** + * @notice set flag to determine if the loan LP liquidity should be used first + * @param newLoanLpFirst new flag to determine if the loan LP liquidity should be used first + */ + function setPreferLoanLiquidity(bool newLoanLpFirst) external; } diff --git a/contracts/interfaces/IRedemptionVaultWithSwapper.sol b/contracts/interfaces/IRedemptionVaultWithSwapper.sol deleted file mode 100644 index 9fcbd29e..00000000 --- a/contracts/interfaces/IRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "./IRedemptionVault.sol"; - -/** - * @title IRedemptionVaultWithSwapper - * @author RedDuck Software - */ -interface IRedemptionVaultWithSwapper is IRedemptionVault { - /** - * @param caller caller address (msg.sender) - * @param provider new LP address - */ - event SetLiquidityProvider( - address indexed caller, - address indexed provider - ); - - /** - * @param caller caller address (msg.sender) - * @param vault new underlying vault for swapper - */ - event SetSwapperVault(address indexed caller, address indexed vault); - - /** - * @notice sets new liquidity provider address - * @param provider new liquidity provider address - */ - function setLiquidityProvider(address provider) external; - - /** - * @notice sets new underlying vault for swapper - * @param vault new underlying vault for swapper - */ - function setSwapperVault(address vault) external; -} diff --git a/contracts/interfaces/ISanctionsList.sol b/contracts/interfaces/ISanctionsList.sol index 5f30dcc9..d0e6853f 100644 --- a/contracts/interfaces/ISanctionsList.sol +++ b/contracts/interfaces/ISanctionsList.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +pragma solidity 0.8.34; -// TODO: add natspec +/** + * @notice Chainalysis sanctions oracle interface + */ interface ISanctionsList { function isSanctioned(address addr) external view returns (bool); } diff --git a/contracts/interfaces/aave/IAaveV3Pool.sol b/contracts/interfaces/aave/IAaveV3Pool.sol index c239da0f..5c208b91 100644 --- a/contracts/interfaces/aave/IAaveV3Pool.sol +++ b/contracts/interfaces/aave/IAaveV3Pool.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +pragma solidity 0.8.34; /** * @title IAaveV3Pool diff --git a/contracts/interfaces/acre/IAcreAdapter.sol b/contracts/interfaces/acre/IAcreAdapter.sol index fd0ab003..20e93711 100644 --- a/contracts/interfaces/acre/IAcreAdapter.sol +++ b/contracts/interfaces/acre/IAcreAdapter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +pragma solidity 0.8.34; /** * @title IAcreAdapter diff --git a/contracts/interfaces/buidl/IRedemption.sol b/contracts/interfaces/buidl/IRedemption.sol deleted file mode 100644 index be026f89..00000000 --- a/contracts/interfaces/buidl/IRedemption.sol +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright 2024 Circle Internet Financial, LTD. All rights reserved. - * - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -pragma solidity 0.8.9; - -/** - * @title IRedemption - */ -interface IRedemption { - /** - * @notice The asset being redeemed. - * @return The address of the asset token. - */ - function asset() external view returns (address); - - /** - * @notice The liquidity token that the asset is being redeemed for. - * @return The address of the liquidity token. - */ - function liquidity() external view returns (address); - - /** - * @notice The settlement contract address. - * @return The address of the settlement contract. - */ - function settlement() external view returns (address); - - /** - * @notice Redeems an amount of asset for liquidity - * @param amount The amount of the asset token to redeem - */ - function redeem(uint256 amount) external; -} diff --git a/contracts/interfaces/morpho/IMorphoVault.sol b/contracts/interfaces/morpho/IMorphoVault.sol index 34e78c67..120f1dcd 100644 --- a/contracts/interfaces/morpho/IMorphoVault.sol +++ b/contracts/interfaces/morpho/IMorphoVault.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +pragma solidity 0.8.34; -import "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; /** * @title IMorphoVault diff --git a/contracts/interfaces/ustb/IAllowListV2.sol b/contracts/interfaces/ustb/IAllowListV2.sol index 4f1fa47e..b9ccb927 100644 --- a/contracts/interfaces/ustb/IAllowListV2.sol +++ b/contracts/interfaces/ustb/IAllowListV2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +pragma solidity 0.8.34; interface IAllowListV2 { type EntityId is uint256; diff --git a/contracts/interfaces/ustb/ISuperstateToken.sol b/contracts/interfaces/ustb/ISuperstateToken.sol index d13b6a08..41aa96c8 100644 --- a/contracts/interfaces/ustb/ISuperstateToken.sol +++ b/contracts/interfaces/ustb/ISuperstateToken.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +pragma solidity 0.8.34; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/interfaces/ustb/IUSTBRedemption.sol b/contracts/interfaces/ustb/IUSTBRedemption.sol index 59588410..6929b0d1 100644 --- a/contracts/interfaces/ustb/IUSTBRedemption.sol +++ b/contracts/interfaces/ustb/IUSTBRedemption.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +pragma solidity 0.8.34; interface IUSTBRedemption { function SUPERSTATE_TOKEN() external view returns (address); diff --git a/contracts/libraries/DecimalsCorrectionLibrary.sol b/contracts/libraries/DecimalsCorrectionLibrary.sol index 0481385d..b6adb8b9 100644 --- a/contracts/libraries/DecimalsCorrectionLibrary.sol +++ b/contracts/libraries/DecimalsCorrectionLibrary.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity 0.8.34; /** * @title DecimalsCorrectionLibrary diff --git a/contracts/libraries/MidasAuthLibrary.sol b/contracts/libraries/MidasAuthLibrary.sol new file mode 100644 index 00000000..9279f4ee --- /dev/null +++ b/contracts/libraries/MidasAuthLibrary.sol @@ -0,0 +1,366 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.34; + +import {IMidasAccessControl} from "../interfaces/IMidasAccessControl.sol"; +import {IMidasTimelockManager} from "../interfaces/IMidasTimelockManager.sol"; + +/** + * @title MidasAuthLibrary + * @author RedDuck Software + */ +library MidasAuthLibrary { + /** + * @notice error when the function permission is not found + * @param roleUsed role used + * @param functionSelector function selector + * @param account account + */ + error NoFunctionPermission( + bytes32 roleUsed, + bytes4 functionSelector, + address account + ); + + /** + * @notice error when the account is not greenlisted + * @param account account + * @param greenlistedRole greenlisted role + */ + error NotGreenlisted(address account, bytes32 greenlistedRole); + + /** + * @notice error when the account is blacklisted + * @param blacklistedRole blacklisted role + * @param account account + */ + error Blacklisted(bytes32 blacklistedRole, address account); + + /** + * @notice error when the sender is not the timelock + * @param roleUsed role used + * @param functionSelector function selector + * @param sender sender + */ + error SenderIsNotTimelock( + bytes32 roleUsed, + bytes4 functionSelector, + address sender + ); + + /** + * @notice error when the user facing role is not allowed + * @param role role + */ + error UserFacingRoleNotAllowed(bytes32 role); + + /** + * @notice error when the delay is invalid + */ + error InvalidDelay(); + + /** + * @notice default role for greenlisted actor + */ + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant DEFAULT_GREENLISTED_ROLE = + keccak256("GREENLISTED_ROLE"); + + /** + * @notice default role for blacklisted actor + */ + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant DEFAULT_BLACKLISTED_ROLE = + keccak256("BLACKLISTED_ROLE"); + + /** + * @notice timelock value that represents no delay + */ + // solhint-disable-next-line private-vars-leading-underscore + uint32 internal constant NO_DELAY = type(uint32).max; + + /** + * @notice timelock value that represents non-set delay + */ + // solhint-disable-next-line private-vars-leading-underscore + uint32 internal constant NULL_DELAY = 0; + + /** + * @notice maximum delay for a role + */ + // solhint-disable-next-line private-vars-leading-underscore + uint32 internal constant MAX_DELAY = 7 days; + + /** + * @dev validates that the function access is valid with timelock + * @param accessControl access control contract + * @param contractAdminRole contract admin role + * @param roleIsFunctionOperatorRole whether the role is a function operator + * @param accountToCheck account to check + * @param validateFunctionRole whether to validate the function role + * @return actualAccount actual account that has access to the function + */ + function validateFunctionAccessWithTimelock( + IMidasAccessControl accessControl, + bytes32 contractAdminRole, + uint32 overrideDelay, + bool roleIsFunctionOperatorRole, + address accountToCheck, + bool validateFunctionRole + ) + internal + view + returns ( + address /* actualAccount */ + ) + { + IMidasTimelockManager timelockManager = IMidasTimelockManager( + accessControl.timelockManager() + ); + + bool isPreflight = accountToCheck == address(timelockManager); + + if (isPreflight) { + revert IMidasTimelockManager.RolePreflightSucceeded( + contractAdminRole, + overrideDelay, + roleIsFunctionOperatorRole, + validateFunctionRole + ); + } + + bool isTimelock = accountToCheck == timelockManager.timelock(); + + address account = isTimelock + ? timelockManager.getOriginalProposer(address(this), msg.data) + : accountToCheck; + + bytes32 roleUsed = validateFunctionAccess( + accessControl, + address(this), + contractAdminRole, + overrideDelay, + roleIsFunctionOperatorRole, + account, + msg.sig, + validateFunctionRole + ); + + (uint32 delay, ) = accessControl.getRoleTimelockDelay( + roleUsed, + overrideDelay + ); + + if (delay > 0) { + require( + isTimelock, + SenderIsNotTimelock(roleUsed, msg.sig, accountToCheck) + ); + } + + return account; + } + + /** + * @dev validates that the function access is valid + * @param accessControl access control contract + * @param role admin role + * @param overrideDelay override delay for the invocation + * @param roleIsFunctionOperatorRole whether the role is a function operator role + * @param account account to check + * @param functionSelector function selector + * @param validateFunctionRole whether to validate the function role + * @return roleUsed role used to validate the function access + */ + function validateFunctionAccess( + IMidasAccessControl accessControl, + address targetContract, + bytes32 role, + uint32 overrideDelay, + bool roleIsFunctionOperatorRole, + address account, + bytes4 functionSelector, + bool validateFunctionRole + ) + internal + view + returns ( + bytes32 /* roleUsed */ + ) + { + validateTimelockDelay(overrideDelay); + + if (roleIsFunctionOperatorRole) { + if (accessControl.isFunctionAccessGrantOperator(role, account)) { + return role; + } + revert NoFunctionPermission(role, functionSelector, account); + } + + requireNotUserFacingRole(accessControl, role); + + bool hasRootRole = accessControl.hasRole(role, account); + + (bytes32 key, bool hasPermission) = validateFunctionRole + ? _hasFunctionPermission( + accessControl, + targetContract, + role, + functionSelector, + account + ) + : (bytes32(0), false); + + if (!hasPermission && !hasRootRole) { + revert NoFunctionPermission(role, functionSelector, account); + } + + if (!hasRootRole) { + return key; + } + + if (!hasPermission) { + return role; + } + + return resolveAccessRole(accessControl, role, key, overrideDelay); + } + + /** + * @dev validates that the role is not a user facing role + * @param accessControl access control contract + * @param role role + */ + function requireNotUserFacingRole( + IMidasAccessControl accessControl, + bytes32 role + ) internal view { + require( + !accessControl.isUserFacingRole(role), + UserFacingRoleNotAllowed(role) + ); + } + + /** + * @dev checks that a given `account` has `greenlistedRole` + * @param accessControl access control contract + * @param account account + * @param greenlistedRole greenlisted role + */ + function requireGreenlisted( + IMidasAccessControl accessControl, + address account, + bytes32 greenlistedRole + ) internal view { + require( + accessControl.hasRole(greenlistedRole, account), + NotGreenlisted(account, greenlistedRole) + ); + } + + /** + * @dev checks that a given `account` doesnt have `blacklistedRole` + * @param accessControl access control contract + * @param account account + * @param blacklistedRole blacklisted role + */ + function requireNotBlacklisted( + IMidasAccessControl accessControl, + address account, + bytes32 blacklistedRole + ) internal view { + require( + !accessControl.hasRole(blacklistedRole, account), + Blacklisted(blacklistedRole, account) + ); + } + + /** + * @dev resolves the access role based on the shortest delay + * @param accessControl access control contract + * @param rootRole root role + * @param functionRoleKey function key + * @param overrideDelay override delay + * @return roleUsed role used to validate the function access + */ + function resolveAccessRole( + IMidasAccessControl accessControl, + bytes32 rootRole, + bytes32 functionRoleKey, + uint32 overrideDelay + ) internal view returns (bytes32 roleUsed) { + if (overrideDelay != NULL_DELAY) { + return rootRole; + } + (uint32 rootDelay, ) = accessControl.getRoleTimelockDelay( + rootRole, + overrideDelay + ); + (uint32 functionDelay, ) = accessControl.getRoleTimelockDelay( + functionRoleKey, + overrideDelay + ); + return rootDelay <= functionDelay ? rootRole : functionRoleKey; + } + + /** + * @notice validates that the delay is within the maximum delay + * @param delay delay to validate + */ + function validateTimelockDelay(uint32 delay) internal view { + if (delay != NO_DELAY) { + require(delay <= MAX_DELAY, InvalidDelay()); + } + } + + /** + * @dev checks that given `account` has function permission for the given function selector + * @param accessControl access control contract + * @param role OZ role for the scope + * @param targetContract scoped contract + * @param functionSelector function selector + * @param account address checked for permission + */ + function _hasFunctionPermission( + IMidasAccessControl accessControl, + address targetContract, + bytes32 role, + bytes4 functionSelector, + address account + ) private view returns (bytes32 key, bool hasPermission) { + key = accessControl.permissionRoleKey( + role, + targetContract, + functionSelector + ); + + hasPermission = accessControl.hasFunctionPermission(key, account); + } + + /** + * @dev appends the proposer to the data + * @param data operation data + * @param proposer proposer address + * @return appended data + */ + function appendProposer(bytes calldata data, address proposer) + internal + pure + returns (bytes memory) + { + return abi.encodePacked(data, proposer); + } + + /** + * @dev resolves the proposer from the data + * @param data data + * @return proposer proposer address + */ + function resolveProposer(bytes calldata data) + internal + pure + returns (address proposer) + { + return address(bytes20(data[data.length - 20:])); + } +} diff --git a/contracts/libraries/PauseGuardsLibrary.sol b/contracts/libraries/PauseGuardsLibrary.sol new file mode 100644 index 00000000..6c32b154 --- /dev/null +++ b/contracts/libraries/PauseGuardsLibrary.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; + +import {IMidasAccessControl} from "../interfaces/IMidasAccessControl.sol"; +import {IMidasPauseManager} from "../interfaces/IMidasPauseManager.sol"; + +/** + * @title PauseGuardsLibrary + * @notice library for checking pause statuses + * @author RedDuck Software + */ +library PauseGuardsLibrary { + /** + * @notice error thrown when a function is paused + * @param contractAddr contract address + * @param fn function id + */ + error Paused(address contractAddr, bytes4 fn); + + /** + * @dev checks that a given `fn` is not paused + * @param fn function id + */ + function requireFnNotPaused(IMidasAccessControl accessControl, bytes4 fn) + internal + view + { + require( + !IMidasPauseManager(accessControl.pauseManager()).isFunctionPaused( + address(this), + fn + ), + Paused(address(this), fn) + ); + } + + /** + * @dev checks that a given `fn` and contract/global are not paused + * @param fn function id + */ + function requireNotPaused(IMidasAccessControl accessControl, bytes4 fn) + internal + view + { + require( + !IMidasPauseManager(accessControl.pauseManager()).isPaused( + address(this), + fn + ), + Paused(address(this), fn) + ); + } +} diff --git a/contracts/libraries/RateLimitLibrary.sol b/contracts/libraries/RateLimitLibrary.sol new file mode 100644 index 00000000..8b76d9c3 --- /dev/null +++ b/contracts/libraries/RateLimitLibrary.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; + +import {EnumerableSetUpgradeable as EnumerableSet} from "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; +import {MathUpgradeable as Math} from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; + +/** + * @title RateLimitLibrary + * @author RedDuck Software + * @notice Multi-window linear-decay rate limiting (vault instant flows, mToken mint, etc.). + */ +library RateLimitLibrary { + using EnumerableSet for EnumerableSet.UintSet; + + uint256 private constant _MIN_WINDOW = 1 minutes; + + /** + * @notice when window limit is exceeded + * @param window window duration in seconds + * @param remaining actual remaining amount + * @param requested requested amount + */ + error WindowLimitExceeded( + uint256 window, + uint256 remaining, + uint256 requested + ); + + /** + * @notice when window limit is unknown + * @param window window duration in seconds + */ + error UnknownWindowLimit(uint256 window); + + /** + * @notice when window is too short + * @param window window duration in seconds + */ + error WindowTooShort(uint256 window); + + /** + * @notice Emitted when a window limit is set or updated. + * @param window window duration in seconds + * @param limit max amount per window + */ + event WindowLimitSet(uint256 window, uint256 limit); + + /** + * @notice Emitted when a window limit is removed. + * @param window window duration in seconds + */ + event WindowLimitRemoved(uint256 window); + + /** + * @notice Per-window rate limit (linear decay over `window` seconds). + */ + struct WindowRateLimitConfig { + /// @notice max amount per window + uint256 limit; + /// @notice amount currently counted against the limit + uint256 amountInFlight; + /// @notice last block timestamp when the limit was checked or updated + uint256 lastUpdated; + /// @notice window duration in seconds + uint256 window; + } + + /** + * @notice Active windows and their configs (keyed by window duration). + */ + struct WindowRateLimits { + EnumerableSet.UintSet windows; + mapping(uint256 => WindowRateLimitConfig) configs; + } + + /** + * @notice Snapshot for one window (view helper). + */ + struct WindowRateLimitStatus { + /// @notice decayed in-flight amount + uint256 inFlight; + /// @notice headroom under `limit` + uint256 remaining; + /// @notice last update timestamp + uint256 lastUpdated; + /// @notice window duration in seconds + uint256 window; + /// @notice max amount per window + uint256 limit; + } + + /** + * @notice Returns a status row per configured window (enumeration order). + * @param limits aggregated window state + * @return statuses one entry per active window + */ + function getWindowStatuses(WindowRateLimits storage limits) + internal + view + returns (WindowRateLimitStatus[] memory statuses) + { + uint256 n = limits.windows.length(); + statuses = new WindowRateLimitStatus[](n); + for (uint256 i = 0; i < n; ++i) { + uint256 window = limits.windows.at(i); + statuses[i] = _getWindowStatus(limits.configs[window]); + } + } + + /** + * @notice Sets or updates the limit for a window (checkpoints first). + * @param limits aggregated window state + * @param window window duration in seconds + * @param limit max amount per window + * @return previousLimit previous limit + */ + function setWindowLimit( + WindowRateLimits storage limits, + uint256 window, + uint256 limit + ) internal returns (uint256 previousLimit) { + if (limits.windows.add(window)) { + require(window >= _MIN_WINDOW, WindowTooShort(window)); + } + + WindowRateLimitConfig storage cfg = limits.configs[window]; + + previousLimit = cfg.limit; + + cfg.window = window; + cfg.limit = limit; + + _consumeWindowLimit(cfg, 0); + + emit WindowLimitSet(window, limit); + } + + /** + * @notice Removes a window and clears its config. + * @param limits aggregated window state + * @param window window duration in seconds + */ + function removeWindowLimit(WindowRateLimits storage limits, uint256 window) + internal + { + require(limits.windows.remove(window), UnknownWindowLimit(window)); + delete limits.configs[window]; + emit WindowLimitRemoved(window); + } + + /** + * @notice Charges `amount` against every window (reverts if any lacks headroom). + * @param limits aggregated window state + * @param amount amount to charge + */ + function consumeLimit(WindowRateLimits storage limits, uint256 amount) + internal + { + uint256 n = limits.windows.length(); + for (uint256 i = 0; i < n; ++i) { + uint256 window = limits.windows.at(i); + _consumeWindowLimit(limits.configs[window], amount); + } + } + + /** + * @notice Decayed in-flight and remaining headroom for one window. + * @param cfg stored window config + * @return status window status snapshot + */ + function _getWindowStatus(WindowRateLimitConfig storage cfg) + private + view + returns (WindowRateLimitStatus memory status) + { + (uint256 inFlight, uint256 remaining) = _availableCapacity( + cfg.amountInFlight, + cfg.lastUpdated, + cfg.limit, + cfg.window + ); + + status = WindowRateLimitStatus({ + inFlight: inFlight, + remaining: remaining, + lastUpdated: cfg.lastUpdated, + window: cfg.window, + limit: cfg.limit + }); + } + + /** + * @notice Linear decay (`limit * elapsed / window`, floored) then headroom. + * @param amountInFlight stored in-flight amount + * @param lastUpdated last update timestamp + * @param limit max per window + * @param window window duration in seconds + * @return inFlight decayed in-flight + * @return remaining capacity still available under `limit` + */ + function _availableCapacity( + uint256 amountInFlight, + uint256 lastUpdated, + uint256 limit, + uint256 window + ) private view returns (uint256 inFlight, uint256 remaining) { + uint256 elapsed = block.timestamp - lastUpdated; + + uint256 decay = Math.mulDiv( + limit, + elapsed, + window > 0 ? window : 1, + Math.Rounding.Down + ); + + inFlight = amountInFlight <= decay ? 0 : amountInFlight - decay; + + remaining = limit <= inFlight ? 0 : limit - inFlight; + } + + /** + * @notice Checks headroom, then adds `amount` to in-flight and refreshes timestamp. + * @param cfg stored window config + * @param amount amount to add to in-flight (may be zero to checkpoint) + */ + function _consumeWindowLimit( + WindowRateLimitConfig storage cfg, + uint256 amount + ) private { + ( + uint256 amountInFlight, + uint256 lastUpdated, + uint256 limit, + uint256 window + ) = (cfg.amountInFlight, cfg.lastUpdated, cfg.limit, cfg.window); + + (uint256 inFlight, uint256 remaining) = _availableCapacity( + amountInFlight, + lastUpdated, + limit, + window + ); + + require( + amount <= remaining, + WindowLimitExceeded(window, remaining, amount) + ); + + cfg.amountInFlight = inFlight + amount; + cfg.lastUpdated = block.timestamp; + } +} diff --git a/contracts/libraries/RedemptionSwapperHelpersLibrary.sol b/contracts/libraries/RedemptionSwapperHelpersLibrary.sol new file mode 100644 index 00000000..33da28e9 --- /dev/null +++ b/contracts/libraries/RedemptionSwapperHelpersLibrary.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; + +import {IRedemptionVault} from "../interfaces/IRedemptionVault.sol"; +import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {DecimalsCorrectionLibrary} from "./DecimalsCorrectionLibrary.sol"; + +library RedemptionSwapperHelpersLibrary { + using SafeERC20 for IERC20; + using DecimalsCorrectionLibrary for uint256; + + function getSwapperDetails( + IRedemptionVault _redemptionVault, + address _getBalanceOf + ) + internal + view + returns ( + uint256 mTokenARate, + IERC20 mTokenA, + uint256 mTokenABalance + ) + { + mTokenARate = _redemptionVault.mTokenDataFeed().getDataInBase18(); + mTokenA = IERC20(address(_redemptionVault.mToken())); + mTokenABalance = mTokenA.balanceOf(_getBalanceOf); + } + + function redeemInstantSwapper( + IRedemptionVault _swapperVault, + IERC20 _mTokenA, + address _liquiditySource, + address _tokenOut, + uint256 _mTokenAAmount, + uint256 _tokenOutDecimals + ) internal returns (uint256) { + if (_liquiditySource != address(this)) { + _mTokenA.safeTransferFrom( + _liquiditySource, + address(this), + _mTokenAAmount + ); + } + + _mTokenA.safeIncreaseAllowance(address(_swapperVault), _mTokenAAmount); + + return + _swapperVault + .redeemInstant(_tokenOut, _mTokenAAmount, 0) + .convertToBase18(_tokenOutDecimals); + } +} diff --git a/contracts/mToken.sol b/contracts/mToken.sol index 05ed0545..a28072e3 100644 --- a/contracts/mToken.sol +++ b/contracts/mToken.sol @@ -1,35 +1,183 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; +import {ERC20PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "./access/Blacklistable.sol"; -import "./interfaces/IMToken.sol"; +import {RateLimitLibrary} from "./libraries/RateLimitLibrary.sol"; +import {MidasAuthLibrary} from "./libraries/MidasAuthLibrary.sol"; +import {IMidasAccessControl} from "./interfaces/IMidasAccessControl.sol"; +import {PauseGuardsLibrary} from "./libraries/PauseGuardsLibrary.sol"; +import {MidasInitializable} from "./abstract/MidasInitializable.sol"; +import {WithMidasAccessControl} from "./access/WithMidasAccessControl.sol"; +import {Blacklistable} from "./access/Blacklistable.sol"; +import {IMToken} from "./interfaces/IMToken.sol"; /** * @title mToken * @author RedDuck Software */ //solhint-disable contract-name-camelcase -abstract contract mToken is ERC20PausableUpgradeable, Blacklistable, IMToken { +contract mToken is ERC20PausableUpgradeable, Blacklistable, IMToken { + using RateLimitLibrary for RateLimitLibrary.WindowRateLimits; + using MidasAuthLibrary for IMidasAccessControl; + + /** + * @dev role that grants contract admin rights to the contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _CONTRACT_ADMIN_ROLE; + + /** + * @dev role that grants minter rights to the contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _MINTER_ROLE; + + /** + * @dev role that grants burner rights to the contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _BURNER_ROLE; + /** * @notice metadata key => metadata value */ mapping(bytes32 => bytes) public metadata; + /** + * @notice mint rate limits state + */ + RateLimitLibrary.WindowRateLimits private _mintRateLimits; + + /** + * @notice address to which clawback tokens will be sent + */ + address public clawbackReceiver; + + /** + * @notice if true then current transfer is clawback operation + */ + bool internal _inClawback; + + /** + * @notice name of the token + */ + string private _name; + + /** + * @notice symbol of the token + */ + string private _symbol; + /** * @dev leaving a storage gap for futures updates */ - uint256[50] private __gap; + uint256[44] private __gap; + + /** + * @dev having a second gap here to match with the gap of previous implementations + */ + uint256[50] private ___gap; + + /** + * @notice constructor + * @param _contractAdminRole contract admin role + * @param _minterRole minter role + * @param _burnerRole burner role + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor( + bytes32 _contractAdminRole, + bytes32 _minterRole, + bytes32 _burnerRole + ) MidasInitializable() { + _CONTRACT_ADMIN_ROLE = _contractAdminRole; + _MINTER_ROLE = _minterRole; + _BURNER_ROLE = _burnerRole; + } /** * @notice upgradeable pattern contract`s initializer * @param _accessControl address of MidasAccessControll contract + * @param _clawbackReceiver address to which clawback tokens will be sent + * @param name_ name of the token + * @param symbol_ symbol of the token */ - function initialize(address _accessControl) external virtual initializer { - __Blacklistable_init(_accessControl); - (string memory _name, string memory _symbol) = _getNameSymbol(); - __ERC20_init(_name, _symbol); + function initialize( + address _accessControl, + address _clawbackReceiver, + string memory name_, + string memory symbol_ + ) external { + _initializeV1(_accessControl, name_, symbol_); + initializeV2(_clawbackReceiver); + } + + /** + * @dev v1 initializer + * @param _accessControl address of MidasAccessControll contract + * @param name_ name of the token + * @param symbol_ symbol of the token + */ + function _initializeV1( + address _accessControl, + string memory name_, + string memory symbol_ + ) private initializer { + __WithMidasAccessControl_init(_accessControl); + __ERC20_init(name_, symbol_); + } + + /** + * @dev v2 initializer + * @param _clawbackReceiver address to which clawback tokens will be sent + */ + function initializeV2(address _clawbackReceiver) + public + virtual + reinitializer(3) + onlyProxyAdmin + { + require( + _clawbackReceiver != address(0), + InvalidAddress(_clawbackReceiver) + ); + + clawbackReceiver = _clawbackReceiver; + + // to make upgrades safer, we sync the name and symbol from the ERC20Upgradeable + _name = ERC20Upgradeable.name(); + _symbol = ERC20Upgradeable.symbol(); + } + + /** + * @inheritdoc IMToken + */ + function setNameSymbol(string memory name_, string memory symbol_) + external + onlyRoleDelayOverride(contractAdminRole(), 2 days, false) + { + _name = name_; + _symbol = symbol_; + } + + /** + * @inheritdoc IMToken + */ + function setClawbackReceiver(address _clawbackReceiver) + external + onlyContractAdmin + { + require( + _clawbackReceiver != address(0), + InvalidAddress(_clawbackReceiver) + ); + clawbackReceiver = _clawbackReceiver; + emit ClawbackReceiverSet(_clawbackReceiver); } /** @@ -37,7 +185,17 @@ abstract contract mToken is ERC20PausableUpgradeable, Blacklistable, IMToken { */ function mint(address to, uint256 amount) external - onlyRole(_minterRole(), msg.sender) + onlyRoleNoTimelock(minterRole(), false) + { + _mint(to, amount); + } + + /** + * @inheritdoc IMToken + */ + function mintGoverned(address to, uint256 amount) + external + onlyContractAdmin { _mint(to, amount); } @@ -47,23 +205,29 @@ abstract contract mToken is ERC20PausableUpgradeable, Blacklistable, IMToken { */ function burn(address from, uint256 amount) external - onlyRole(_burnerRole(), msg.sender) + onlyRoleNoTimelock(burnerRole(), false) { + _onlyNotBlacklisted(from); _burn(from, amount); } /** * @inheritdoc IMToken */ - function pause() external override onlyRole(_pauserRole(), msg.sender) { - _pause(); + function burnGoverned(address from, uint256 amount) + external + onlyContractAdmin + { + _burn(from, amount); } /** * @inheritdoc IMToken */ - function unpause() external override onlyRole(_pauserRole(), msg.sender) { - _unpause(); + function clawback(uint256 amount, address from) external onlyContractAdmin { + _inClawback = true; + _transfer(from, clawbackReceiver, amount); + _inClawback = false; } /** @@ -71,50 +235,139 @@ abstract contract mToken is ERC20PausableUpgradeable, Blacklistable, IMToken { */ function setMetadata(bytes32 key, bytes memory data) external - onlyRole(DEFAULT_ADMIN_ROLE, msg.sender) + onlyContractAdmin { metadata[key] = data; } /** - * @dev overrides _beforeTokenTransfer function to ban - * blaclisted users from using the token functions + * @inheritdoc IMToken */ - function _beforeTokenTransfer( - address from, - address to, - uint256 amount - ) internal virtual override(ERC20PausableUpgradeable) { - if (to != address(0)) { - _onlyNotBlacklisted(from); - _onlyNotBlacklisted(to); - } + function increaseMintRateLimit(uint256 window, uint256 newLimit) + external + onlyContractAdmin + { + _setMintRateLimitConfig(window, newLimit, true); + } - ERC20PausableUpgradeable._beforeTokenTransfer(from, to, amount); + /** + * @inheritdoc IMToken + */ + function decreaseMintRateLimit(uint256 window, uint256 newLimit) + external + onlyContractAdmin + { + _setMintRateLimitConfig(window, newLimit, false); + } + + /** + * @inheritdoc IMToken + */ + function removeMintRateLimitConfig(uint256 window) + external + onlyContractAdmin + { + _mintRateLimits.removeWindowLimit(window); + } + + /** + * @notice returns array of mint rate limit configs + * @return statuses array of mint rate limit statuses + */ + function getMintRateLimitStatuses() + external + view + returns ( + RateLimitLibrary.WindowRateLimitStatus[] memory /* statuses */ + ) + { + return _mintRateLimits.getWindowStatuses(); } /** - * @dev returns name and symbol of the token - * @return name of the token - * @return symbol of the token + * @notice AC role, owner of which can mint mToken token */ - function _getNameSymbol() - internal + function minterRole() public view virtual returns (bytes32) { + return _MINTER_ROLE; + } + + /** + * @notice AC role, owner of which can burn mToken token + */ + function burnerRole() public view virtual returns (bytes32) { + return _BURNER_ROLE; + } + + /** + * @inheritdoc WithMidasAccessControl + */ + function contractAdminRole() + public + view virtual - returns (string memory, string memory); + override + returns (bytes32) + { + return _CONTRACT_ADMIN_ROLE; + } + + /** + * @inheritdoc ERC20Upgradeable + */ + function name() public view virtual override returns (string memory) { + return _name; + } /** - * @dev AC role, owner of which can mint mToken token + * @inheritdoc ERC20Upgradeable */ - function _minterRole() internal pure virtual returns (bytes32); + function symbol() public view virtual override returns (string memory) { + return _symbol; + } /** - * @dev AC role, owner of which can burn mToken token + * @dev set mint rate limit config + * @param window window duration in seconds + * @param limit limit amount per window + * @param increaseOnly if true - only increase the limit, if false - only decrease the limit */ - function _burnerRole() internal pure virtual returns (bytes32); + function _setMintRateLimitConfig( + uint256 window, + uint256 limit, + bool increaseOnly + ) private { + uint256 previousLimit = _mintRateLimits.setWindowLimit(window, limit); + + bool isNewLimitValid = increaseOnly + ? limit > previousLimit + : limit < previousLimit; + + require(isNewLimitValid, InvalidNewLimit(limit, previousLimit)); + } /** - * @dev AC role, owner of which can pause mToken token + * @dev overrides _beforeTokenTransfer function to ban + * blaclisted users from using the token functions */ - function _pauserRole() internal pure virtual returns (bytes32); + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override(ERC20PausableUpgradeable) { + PauseGuardsLibrary.requireNotPaused(accessControl, msg.sig); + + if (to != address(0)) { + if (!_inClawback && from != address(0)) { + _onlyNotBlacklisted(from); + } + _onlyNotBlacklisted(to); + } + + // if minting, check and update mint rate limit + if (from == address(0)) { + _mintRateLimits.consumeLimit(amount); + } + + ERC20PausableUpgradeable._beforeTokenTransfer(from, to, amount); + } } diff --git a/contracts/mTokenPermissioned.sol b/contracts/mTokenPermissioned.sol index 88d9a5a8..8c15a5eb 100644 --- a/contracts/mTokenPermissioned.sol +++ b/contracts/mTokenPermissioned.sol @@ -1,7 +1,9 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "./mToken.sol"; +import {MidasAuthLibrary} from "./libraries/MidasAuthLibrary.sol"; + +import {mToken} from "./mToken.sol"; /** * @title mTokenPermissioned @@ -9,12 +11,44 @@ import "./mToken.sol"; * @author RedDuck Software */ //solhint-disable contract-name-camelcase -abstract contract mTokenPermissioned is mToken { +contract mTokenPermissioned is mToken { + /** + * @dev role that grants greenlisted rights to the contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _GREENLISTED_ROLE; + /** * @dev leaving a storage gap for futures updates */ uint256[50] private __gap; + /** + * @notice constructor + * @param _contractAdminRole contract admin role + * @param _minterRole minter role + * @param _burnerRole burner role + * @param _greenlistedRole greenlisted role + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor( + bytes32 _contractAdminRole, + bytes32 _minterRole, + bytes32 _burnerRole, + bytes32 _greenlistedRole + ) mToken(_contractAdminRole, _minterRole, _burnerRole) { + _GREENLISTED_ROLE = _greenlistedRole; + } + + /** + * @notice AC role of a greenlist + * @return role bytes32 role + */ + function greenlistedRole() public view virtual returns (bytes32) { + return _GREENLISTED_ROLE; + } + /** * @dev overrides _beforeTokenTransfer function to allow * greenlisted users to use the token transfers functions @@ -25,9 +59,10 @@ abstract contract mTokenPermissioned is mToken { uint256 amount ) internal virtual override(mToken) { if (to != address(0)) { - if (from != address(0)) { + if (!_inClawback && from != address(0)) { _onlyGreenlisted(from); } + _onlyGreenlisted(to); } @@ -35,18 +70,13 @@ abstract contract mTokenPermissioned is mToken { } /** - * @notice AC role of a greenlist - * @return role bytes32 role - */ - function _greenlistedRole() internal pure virtual returns (bytes32); - - /** - * @dev checks that a given `account` - * have `greenlistedRole()` + * @dev checks that a given `account` has `greenlistedRole()` */ - function _onlyGreenlisted(address account) - private - view - onlyRole(_greenlistedRole(), account) - {} + function _onlyGreenlisted(address account) private view { + MidasAuthLibrary.requireGreenlisted( + accessControl, + account, + greenlistedRole() + ); + } } diff --git a/contracts/misc/acre/AcreAdapter.sol b/contracts/misc/acre/AcreAdapter.sol index 36639187..6d5dbcf9 100644 --- a/contracts/misc/acre/AcreAdapter.sol +++ b/contracts/misc/acre/AcreAdapter.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20Metadata, IERC20} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; @@ -109,9 +109,12 @@ contract AcreAdapter is IAcreAdapter { IERC20(share()).safeTransferFrom(msg.sender, address(this), shares); - requestId = IRedemptionVault(redemptionVault).redeemRequest( + (requestId, ) = IRedemptionVault(redemptionVault).redeemRequest( asset(), shares, + receiver, + 0, + 0, receiver ); diff --git a/contracts/misc/adapters/BandStdChailinkAdapter.sol b/contracts/misc/adapters/BandStdChailinkAdapter.sol index 1af56d19..5beb65ee 100644 --- a/contracts/misc/adapters/BandStdChailinkAdapter.sol +++ b/contracts/misc/adapters/BandStdChailinkAdapter.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "./ChainlinkAdapterBase.sol"; @@ -51,7 +51,7 @@ contract BandStdChailinkAdapter is ChainlinkAdapterBase { } function latestTimestamp() public view override returns (uint256) { - return _getBandReferenceData().lastUpdatedBase; + return _getTimestamp(_getBandReferenceData()); } function latestRoundData() @@ -59,24 +59,31 @@ contract BandStdChailinkAdapter is ChainlinkAdapterBase { view override returns ( - uint80 roundId, - int256 answer, - uint256 startedAt, - uint256 updatedAt, - uint80 answeredInRound + uint80, /* roundId */ + int256, /* answer */ + uint256, /* startedAt */ + uint256, /* updatedAt */ + uint80 /* answeredInRound */ ) { IStdReference.ReferenceData memory value = _getBandReferenceData(); - roundId = uint80(value.lastUpdatedBase); + uint256 timestamp = _getTimestamp(value); + uint80 roundId = uint80(timestamp); - return ( - roundId, - int256(value.rate), - value.lastUpdatedBase, - value.lastUpdatedBase, - roundId - ); + return (roundId, int256(value.rate), timestamp, timestamp, roundId); + } + + function _getTimestamp(IStdReference.ReferenceData memory value) + private + view + returns (uint256) + { + // takes the minimum — stalest component determines freshness + return + value.lastUpdatedBase < value.lastUpdatedQuote + ? value.lastUpdatedBase + : value.lastUpdatedQuote; } function _getBandReferenceData() diff --git a/contracts/misc/adapters/BeHypeChainlinkAdapter.sol b/contracts/misc/adapters/BeHypeChainlinkAdapter.sol index f1fe9222..3eb1c74f 100644 --- a/contracts/misc/adapters/BeHypeChainlinkAdapter.sol +++ b/contracts/misc/adapters/BeHypeChainlinkAdapter.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "./ChainlinkAdapterBase.sol"; diff --git a/contracts/misc/adapters/ChainlinkAdapterBase.sol b/contracts/misc/adapters/ChainlinkAdapterBase.sol index 8ed7f942..9b7f1722 100644 --- a/contracts/misc/adapters/ChainlinkAdapterBase.sol +++ b/contracts/misc/adapters/ChainlinkAdapterBase.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; diff --git a/contracts/misc/adapters/CompositeDataFeedToBandStdAdapter.sol b/contracts/misc/adapters/CompositeDataFeedToBandStdAdapter.sol index c549296d..6f9a4ad6 100644 --- a/contracts/misc/adapters/CompositeDataFeedToBandStdAdapter.sol +++ b/contracts/misc/adapters/CompositeDataFeedToBandStdAdapter.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../../interfaces/IDataFeed.sol"; import "../../feeds/CompositeDataFeed.sol"; diff --git a/contracts/misc/adapters/DataFeedToBandStdAdapter.sol b/contracts/misc/adapters/DataFeedToBandStdAdapter.sol index 9f3e1860..34aa0abb 100644 --- a/contracts/misc/adapters/DataFeedToBandStdAdapter.sol +++ b/contracts/misc/adapters/DataFeedToBandStdAdapter.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import "../../interfaces/IDataFeed.sol"; diff --git a/contracts/misc/adapters/ERC4626ChainlinkAdapter.sol b/contracts/misc/adapters/ERC4626ChainlinkAdapter.sol index c63ba72f..f151a20b 100644 --- a/contracts/misc/adapters/ERC4626ChainlinkAdapter.sol +++ b/contracts/misc/adapters/ERC4626ChainlinkAdapter.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "@openzeppelin/contracts/interfaces/IERC4626.sol"; import "./ChainlinkAdapterBase.sol"; diff --git a/contracts/misc/adapters/MantleLspStakingChainlinkAdapter.sol b/contracts/misc/adapters/MantleLspStakingChainlinkAdapter.sol index 0bace543..5655cc0b 100644 --- a/contracts/misc/adapters/MantleLspStakingChainlinkAdapter.sol +++ b/contracts/misc/adapters/MantleLspStakingChainlinkAdapter.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "./ChainlinkAdapterBase.sol"; diff --git a/contracts/misc/adapters/PythChainlinkAdapter.sol b/contracts/misc/adapters/PythChainlinkAdapter.sol index 69b423d5..2c601a76 100644 --- a/contracts/misc/adapters/PythChainlinkAdapter.sol +++ b/contracts/misc/adapters/PythChainlinkAdapter.sol @@ -1,6 +1,6 @@ -// SPDX-License-Identifier: Apache 2 +// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.9; +pragma solidity 0.8.34; import "./ChainlinkAdapterBase.sol"; diff --git a/contracts/misc/adapters/RsEthChainlinkAdapter.sol b/contracts/misc/adapters/RsEthChainlinkAdapter.sol index 873b1f65..6da95955 100644 --- a/contracts/misc/adapters/RsEthChainlinkAdapter.sol +++ b/contracts/misc/adapters/RsEthChainlinkAdapter.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "./ChainlinkAdapterBase.sol"; diff --git a/contracts/misc/adapters/StorkChainlinkAdapter.sol b/contracts/misc/adapters/StorkChainlinkAdapter.sol index 0ae67dfc..162f74c4 100644 --- a/contracts/misc/adapters/StorkChainlinkAdapter.sol +++ b/contracts/misc/adapters/StorkChainlinkAdapter.sol @@ -1,6 +1,6 @@ -// SPDX-License-Identifier: Apache 2 +// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.9; +pragma solidity 0.8.34; import "./ChainlinkAdapterBase.sol"; diff --git a/contracts/misc/adapters/SyrupChainlinkAdapter.sol b/contracts/misc/adapters/SyrupChainlinkAdapter.sol index 6bdc6b26..5047cfd3 100644 --- a/contracts/misc/adapters/SyrupChainlinkAdapter.sol +++ b/contracts/misc/adapters/SyrupChainlinkAdapter.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "./ERC4626ChainlinkAdapter.sol"; diff --git a/contracts/misc/adapters/WrappedEEthChainlinkAdapter.sol b/contracts/misc/adapters/WrappedEEthChainlinkAdapter.sol index 91d1baea..19e9b531 100644 --- a/contracts/misc/adapters/WrappedEEthChainlinkAdapter.sol +++ b/contracts/misc/adapters/WrappedEEthChainlinkAdapter.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "./ChainlinkAdapterBase.sol"; diff --git a/contracts/misc/adapters/WstEthChainlinkAdapter.sol b/contracts/misc/adapters/WstEthChainlinkAdapter.sol index bb48cc37..866b49e8 100644 --- a/contracts/misc/adapters/WstEthChainlinkAdapter.sol +++ b/contracts/misc/adapters/WstEthChainlinkAdapter.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "./ChainlinkAdapterBase.sol"; diff --git a/contracts/misc/adapters/YInjChainlinkAdapter.sol b/contracts/misc/adapters/YInjChainlinkAdapter.sol index 22fd5d9d..2f6d5a88 100644 --- a/contracts/misc/adapters/YInjChainlinkAdapter.sol +++ b/contracts/misc/adapters/YInjChainlinkAdapter.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "./ChainlinkAdapterBase.sol"; diff --git a/contracts/misc/axelar/MidasAxelarVaultExecutable.sol b/contracts/misc/axelar/MidasAxelarVaultExecutable.sol index 893ae6f0..34032673 100644 --- a/contracts/misc/axelar/MidasAxelarVaultExecutable.sol +++ b/contracts/misc/axelar/MidasAxelarVaultExecutable.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.22; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {InterchainTokenExecutable} from "@axelar-network/interchain-token-service/contracts/executable/InterchainTokenExecutable.sol"; import {IInterchainTokenService} from "@axelar-network/interchain-token-service/contracts/interfaces/IInterchainTokenService.sol"; diff --git a/contracts/misc/axelar/interfaces/IMidasAxelarVaultExecutable.sol b/contracts/misc/axelar/interfaces/IMidasAxelarVaultExecutable.sol index 894c2208..af6663e3 100644 --- a/contracts/misc/axelar/interfaces/IMidasAxelarVaultExecutable.sol +++ b/contracts/misc/axelar/interfaces/IMidasAxelarVaultExecutable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.22; +pragma solidity 0.8.34; import {IDepositVault} from "../../../interfaces/IDepositVault.sol"; import {IRedemptionVault} from "../../../interfaces/IRedemptionVault.sol"; diff --git a/contracts/misc/imports.sol b/contracts/misc/imports.sol index c2b373fc..70b0031c 100644 --- a/contracts/misc/imports.sol +++ b/contracts/misc/imports.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; diff --git a/contracts/misc/layerzero/MidasLzMintBurnOFTAdapter.sol b/contracts/misc/layerzero/MidasLzMintBurnOFTAdapter.sol index e1b8eee1..a287ca57 100644 --- a/contracts/misc/layerzero/MidasLzMintBurnOFTAdapter.sol +++ b/contracts/misc/layerzero/MidasLzMintBurnOFTAdapter.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.22; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {MintBurnOFTAdapter} from "@layerzerolabs/oft-evm/contracts/MintBurnOFTAdapter.sol"; diff --git a/contracts/misc/layerzero/MidasLzOFT.sol b/contracts/misc/layerzero/MidasLzOFT.sol index 7ef9be6c..87308b4f 100644 --- a/contracts/misc/layerzero/MidasLzOFT.sol +++ b/contracts/misc/layerzero/MidasLzOFT.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.22; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {OFT} from "@layerzerolabs/oft-evm/contracts/OFT.sol"; diff --git a/contracts/misc/layerzero/MidasLzOFTAdapter.sol b/contracts/misc/layerzero/MidasLzOFTAdapter.sol index 5e34f88f..f045052c 100644 --- a/contracts/misc/layerzero/MidasLzOFTAdapter.sol +++ b/contracts/misc/layerzero/MidasLzOFTAdapter.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.22; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {OFTAdapter} from "@layerzerolabs/oft-evm/contracts/OFTAdapter.sol"; diff --git a/contracts/misc/layerzero/MidasLzVaultComposerSync.sol b/contracts/misc/layerzero/MidasLzVaultComposerSync.sol index ce47b699..b1638ea6 100644 --- a/contracts/misc/layerzero/MidasLzVaultComposerSync.sol +++ b/contracts/misc/layerzero/MidasLzVaultComposerSync.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.22; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import {IERC20MetadataUpgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; diff --git a/contracts/misc/layerzero/interfaces/IMidasLzVaultComposerSync.sol b/contracts/misc/layerzero/interfaces/IMidasLzVaultComposerSync.sol index e11c2439..602b9f17 100644 --- a/contracts/misc/layerzero/interfaces/IMidasLzVaultComposerSync.sol +++ b/contracts/misc/layerzero/interfaces/IMidasLzVaultComposerSync.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.22; +pragma solidity 0.8.34; import {IOAppComposer} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; import {SendParam, MessagingFee} from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; diff --git a/contracts/mocks/AaveV3PoolMock.sol b/contracts/mocks/AaveV3PoolMock.sol index 9d962e7f..901836e2 100644 --- a/contracts/mocks/AaveV3PoolMock.sol +++ b/contracts/mocks/AaveV3PoolMock.sol @@ -1,6 +1,6 @@ // solhint-disable -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/contracts/mocks/AggregatorV3DeprecatedMock.sol b/contracts/mocks/AggregatorV3DeprecatedMock.sol index dba4978e..e2d15e70 100644 --- a/contracts/mocks/AggregatorV3DeprecatedMock.sol +++ b/contracts/mocks/AggregatorV3DeprecatedMock.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; diff --git a/contracts/mocks/AggregatorV3Mock.sol b/contracts/mocks/AggregatorV3Mock.sol index 15eecfbd..306d76eb 100644 --- a/contracts/mocks/AggregatorV3Mock.sol +++ b/contracts/mocks/AggregatorV3Mock.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; diff --git a/contracts/mocks/AggregatorV3UnhealthyMock.sol b/contracts/mocks/AggregatorV3UnhealthyMock.sol index 02eb5436..f0751857 100644 --- a/contracts/mocks/AggregatorV3UnhealthyMock.sol +++ b/contracts/mocks/AggregatorV3UnhealthyMock.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; diff --git a/contracts/mocks/AxelarInterchainTokenServiceMock.sol b/contracts/mocks/AxelarInterchainTokenServiceMock.sol index 1754df03..baf930e8 100644 --- a/contracts/mocks/AxelarInterchainTokenServiceMock.sol +++ b/contracts/mocks/AxelarInterchainTokenServiceMock.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/contracts/mocks/ERC20Mock.sol b/contracts/mocks/ERC20Mock.sol index 0d1f2175..4ad2f444 100644 --- a/contracts/mocks/ERC20Mock.sol +++ b/contracts/mocks/ERC20Mock.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/contracts/mocks/ERC20MockWithName.sol b/contracts/mocks/ERC20MockWithName.sol index 45f1c15e..1ee443ee 100644 --- a/contracts/mocks/ERC20MockWithName.sol +++ b/contracts/mocks/ERC20MockWithName.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/contracts/mocks/LzEndpointV2Mock.sol b/contracts/mocks/LzEndpointV2Mock.sol index bea86c22..a2c8f7d7 100644 --- a/contracts/mocks/LzEndpointV2Mock.sol +++ b/contracts/mocks/LzEndpointV2Mock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: UNLICENSED // solhint-disable -pragma solidity ^0.8.22; +pragma solidity 0.8.34; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {ILayerZeroEndpointV2, MessagingParams, MessagingReceipt, MessagingFee, Origin} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; diff --git a/contracts/mocks/MorphoVaultMock.sol b/contracts/mocks/MorphoVaultMock.sol index 8c8f58d9..905d1326 100644 --- a/contracts/mocks/MorphoVaultMock.sol +++ b/contracts/mocks/MorphoVaultMock.sol @@ -1,6 +1,6 @@ // solhint-disable -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -69,11 +69,20 @@ contract MorphoVaultMock is ERC20 { shares = (assets * RATE_PRECISION) / exchangeRateNumerator; } + function redeem( + uint256 shares, + address receiver, + address owner + ) external returns (uint256 assets) { + assets = convertToAssets(shares); + withdraw(assets, receiver, owner); + } + function withdraw( uint256 assets, address receiver, address owner - ) external returns (uint256 shares) { + ) public returns (uint256 shares) { shares = previewWithdraw(assets); require( @@ -105,7 +114,7 @@ contract MorphoVaultMock is ERC20 { } function convertToAssets(uint256 shares) - external + public view returns (uint256 assets) { diff --git a/contracts/mocks/RedemptionTest.sol b/contracts/mocks/RedemptionTest.sol deleted file mode 100644 index f493b5db..00000000 --- a/contracts/mocks/RedemptionTest.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import "../interfaces/buidl/IRedemption.sol"; - -import "hardhat/console.sol"; - -contract RedemptionTest is IRedemption { - address public asset; - - address public liquidity; - - constructor(address _asset, address _liquidity) { - asset = _asset; - liquidity = _liquidity; - } - - function settlement() external view returns (address) {} - - function redeem(uint256 amount) external { - IERC20(asset).transferFrom(msg.sender, address(this), amount); - IERC20(liquidity).transfer(msg.sender, amount); - } -} diff --git a/contracts/mocks/SanctionsListTest.sol b/contracts/mocks/SanctionsListTest.sol index 97747927..3f53eafb 100644 --- a/contracts/mocks/SanctionsListTest.sol +++ b/contracts/mocks/SanctionsListTest.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../interfaces/ISanctionsList.sol"; diff --git a/contracts/mocks/USTBMock.sol b/contracts/mocks/USTBMock.sol index 337cc2d2..f545c591 100644 --- a/contracts/mocks/USTBMock.sol +++ b/contracts/mocks/USTBMock.sol @@ -1,11 +1,10 @@ // solhint-disable -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../interfaces/ustb/ISuperstateToken.sol"; -import "hardhat/console.sol"; contract USTBMock is ERC20, ISuperstateToken { using SafeERC20 for IERC20; diff --git a/contracts/mocks/USTBRedemptionMock.sol b/contracts/mocks/USTBRedemptionMock.sol index fc186a11..9079f205 100644 --- a/contracts/mocks/USTBRedemptionMock.sol +++ b/contracts/mocks/USTBRedemptionMock.sol @@ -1,6 +1,6 @@ // solhint-disable -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/contracts/mocks/YInjOracleMock.sol b/contracts/mocks/YInjOracleMock.sol index 90481c96..a97c7098 100644 --- a/contracts/mocks/YInjOracleMock.sol +++ b/contracts/mocks/YInjOracleMock.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; contract YInjOracleMock { uint256 private _rate; diff --git a/contracts/products/JIV/JIV.sol b/contracts/products/JIV/JIV.sol deleted file mode 100644 index 9d2e762c..00000000 --- a/contracts/products/JIV/JIV.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title JIV - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract JIV is mToken { - /** - * @notice actor that can mint JIV - */ - bytes32 public constant JIV_MINT_OPERATOR_ROLE = - keccak256("JIV_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn JIV - */ - bytes32 public constant JIV_BURN_OPERATOR_ROLE = - keccak256("JIV_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause JIV - */ - bytes32 public constant JIV_PAUSE_OPERATOR_ROLE = - keccak256("JIV_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Jaine Insurance Vault", "JIV"); - } - - /** - * @dev AC role, owner of which can mint JIV token - */ - function _minterRole() internal pure override returns (bytes32) { - return JIV_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn JIV token - */ - function _burnerRole() internal pure override returns (bytes32) { - return JIV_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause JIV token - */ - function _pauserRole() internal pure override returns (bytes32) { - return JIV_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/JIV/JivCustomAggregatorFeed.sol b/contracts/products/JIV/JivCustomAggregatorFeed.sol deleted file mode 100644 index 1d12e9bb..00000000 --- a/contracts/products/JIV/JivCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./JivMidasAccessControlRoles.sol"; - -/** - * @title JivCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for JIV, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract JivCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - JivMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return JIV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/JIV/JivDataFeed.sol b/contracts/products/JIV/JivDataFeed.sol deleted file mode 100644 index 13f46cac..00000000 --- a/contracts/products/JIV/JivDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./JivMidasAccessControlRoles.sol"; - -/** - * @title JivDataFeed - * @notice DataFeed for JIV product - * @author RedDuck Software - */ -contract JivDataFeed is DataFeed, JivMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return JIV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/JIV/JivDepositVault.sol b/contracts/products/JIV/JivDepositVault.sol deleted file mode 100644 index 07d2b2dc..00000000 --- a/contracts/products/JIV/JivDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./JivMidasAccessControlRoles.sol"; - -/** - * @title JivDepositVault - * @notice Smart contract that handles JIV minting - * @author RedDuck Software - */ -contract JivDepositVault is DepositVault, JivMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return JIV_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/JIV/JivMidasAccessControlRoles.sol b/contracts/products/JIV/JivMidasAccessControlRoles.sol deleted file mode 100644 index 66844eb2..00000000 --- a/contracts/products/JIV/JivMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title JivMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for JIV contracts - * @author RedDuck Software - */ -abstract contract JivMidasAccessControlRoles { - /** - * @notice actor that can manage JivDepositVault - */ - bytes32 public constant JIV_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("JIV_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage JivRedemptionVault - */ - bytes32 public constant JIV_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("JIV_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage JivCustomAggregatorFeed and JivDataFeed - */ - bytes32 public constant JIV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("JIV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/JIV/JivRedemptionVaultWithSwapper.sol b/contracts/products/JIV/JivRedemptionVaultWithSwapper.sol deleted file mode 100644 index 6b71059b..00000000 --- a/contracts/products/JIV/JivRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./JivMidasAccessControlRoles.sol"; - -/** - * @title JivRedemptionVaultWithSwapper - * @notice Smart contract that handles JIV redemptions - * @author RedDuck Software - */ -contract JivRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - JivMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return JIV_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/acremBTC1/AcreMBtc1CustomAggregatorFeed.sol b/contracts/products/acremBTC1/AcreMBtc1CustomAggregatorFeed.sol deleted file mode 100644 index ff960243..00000000 --- a/contracts/products/acremBTC1/AcreMBtc1CustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./AcreMBtc1MidasAccessControlRoles.sol"; - -/** - * @title AcreMBtc1CustomAggregatorFeed - * @notice AggregatorV3 compatible feed for acremBTC1, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract AcreMBtc1CustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - AcreMBtc1MidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return ACRE_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/acremBTC1/AcreMBtc1DataFeed.sol b/contracts/products/acremBTC1/AcreMBtc1DataFeed.sol deleted file mode 100644 index c830bf24..00000000 --- a/contracts/products/acremBTC1/AcreMBtc1DataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./AcreMBtc1MidasAccessControlRoles.sol"; - -/** - * @title AcreMBtc1DataFeed - * @notice DataFeed for acremBTC1 product - * @author RedDuck Software - */ -contract AcreMBtc1DataFeed is DataFeed, AcreMBtc1MidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return ACRE_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/acremBTC1/AcreMBtc1DepositVault.sol b/contracts/products/acremBTC1/AcreMBtc1DepositVault.sol deleted file mode 100644 index 5b2e2848..00000000 --- a/contracts/products/acremBTC1/AcreMBtc1DepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./AcreMBtc1MidasAccessControlRoles.sol"; - -/** - * @title AcreMBtc1DepositVault - * @notice Smart contract that handles acremBTC1 minting - * @author RedDuck Software - */ -contract AcreMBtc1DepositVault is - DepositVault, - AcreMBtc1MidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return ACRE_BTC_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/acremBTC1/AcreMBtc1MidasAccessControlRoles.sol b/contracts/products/acremBTC1/AcreMBtc1MidasAccessControlRoles.sol deleted file mode 100644 index a057bf05..00000000 --- a/contracts/products/acremBTC1/AcreMBtc1MidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title AcreMBtc1MidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for acremBTC1 contracts - * @author RedDuck Software - */ -abstract contract AcreMBtc1MidasAccessControlRoles { - /** - * @notice actor that can manage AcreMBtc1DepositVault - */ - bytes32 public constant ACRE_BTC_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("ACRE_BTC_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage AcreMBtc1RedemptionVault - */ - bytes32 public constant ACRE_BTC_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("ACRE_BTC_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage AcreMBtc1CustomAggregatorFeed and AcreMBtc1DataFeed - */ - bytes32 public constant ACRE_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("ACRE_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/acremBTC1/AcreMBtc1RedemptionVaultWithSwapper.sol b/contracts/products/acremBTC1/AcreMBtc1RedemptionVaultWithSwapper.sol deleted file mode 100644 index fd71a056..00000000 --- a/contracts/products/acremBTC1/AcreMBtc1RedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./AcreMBtc1MidasAccessControlRoles.sol"; - -/** - * @title AcreMBtc1RedemptionVaultWithSwapper - * @notice Smart contract that handles acremBTC1 redemptions - * @author RedDuck Software - */ -contract AcreMBtc1RedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - AcreMBtc1MidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return ACRE_BTC_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/acremBTC1/acremBTC1.sol b/contracts/products/acremBTC1/acremBTC1.sol deleted file mode 100644 index b51f4fdc..00000000 --- a/contracts/products/acremBTC1/acremBTC1.sol +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title acremBTC1 - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract acremBTC1 is mToken { - /** - * @notice actor that can mint acremBTC1 - */ - bytes32 public constant ACRE_BTC_MINT_OPERATOR_ROLE = - keccak256("ACRE_BTC_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn acremBTC1 - */ - bytes32 public constant ACRE_BTC_BURN_OPERATOR_ROLE = - keccak256("ACRE_BTC_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause acremBTC1 - */ - bytes32 public constant ACRE_BTC_PAUSE_OPERATOR_ROLE = - keccak256("ACRE_BTC_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ERC20Upgradeable - * @dev override to return a new name (not the initial one) - */ - function name() public pure override returns (string memory _name) { - (_name, ) = _getNameSymbol(); - } - - /** - * @inheritdoc ERC20Upgradeable - * @dev override to return a new symbol (not the initial one) - */ - function symbol() public pure override returns (string memory _symbol) { - (, _symbol) = _getNameSymbol(); - } - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("acremBTC1", "acremBTC1"); - } - - /** - * @dev AC role, owner of which can mint acremBTC1 token - */ - function _minterRole() internal pure override returns (bytes32) { - return ACRE_BTC_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn acremBTC1 token - */ - function _burnerRole() internal pure override returns (bytes32) { - return ACRE_BTC_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause acremBTC1 token - */ - function _pauserRole() internal pure override returns (bytes32) { - return ACRE_BTC_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/bondBTC/BondBtcCustomAggregatorFeed.sol b/contracts/products/bondBTC/BondBtcCustomAggregatorFeed.sol deleted file mode 100644 index b582fc0f..00000000 --- a/contracts/products/bondBTC/BondBtcCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./BondBtcMidasAccessControlRoles.sol"; - -/** - * @title BondBtcCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for bondBTC, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract BondBtcCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - BondBtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return BOND_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/bondBTC/BondBtcDataFeed.sol b/contracts/products/bondBTC/BondBtcDataFeed.sol deleted file mode 100644 index 063ae3f3..00000000 --- a/contracts/products/bondBTC/BondBtcDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./BondBtcMidasAccessControlRoles.sol"; - -/** - * @title BondBtcDataFeed - * @notice DataFeed for bondBTC product - * @author RedDuck Software - */ -contract BondBtcDataFeed is DataFeed, BondBtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return BOND_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/bondBTC/BondBtcDepositVault.sol b/contracts/products/bondBTC/BondBtcDepositVault.sol deleted file mode 100644 index 2f3ea71d..00000000 --- a/contracts/products/bondBTC/BondBtcDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./BondBtcMidasAccessControlRoles.sol"; - -/** - * @title BondBtcDepositVault - * @notice Smart contract that handles bondBTC minting - * @author RedDuck Software - */ -contract BondBtcDepositVault is DepositVault, BondBtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return BOND_BTC_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/bondBTC/BondBtcMidasAccessControlRoles.sol b/contracts/products/bondBTC/BondBtcMidasAccessControlRoles.sol deleted file mode 100644 index cb3da08d..00000000 --- a/contracts/products/bondBTC/BondBtcMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title BondBtcMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for bondBTC contracts - * @author RedDuck Software - */ -abstract contract BondBtcMidasAccessControlRoles { - /** - * @notice actor that can manage BondBtcDepositVault - */ - bytes32 public constant BOND_BTC_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("BOND_BTC_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage BondBtcRedemptionVault - */ - bytes32 public constant BOND_BTC_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("BOND_BTC_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage BondBtcCustomAggregatorFeed and BondBtcDataFeed - */ - bytes32 public constant BOND_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("BOND_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/bondBTC/BondBtcRedemptionVaultWithSwapper.sol b/contracts/products/bondBTC/BondBtcRedemptionVaultWithSwapper.sol deleted file mode 100644 index d149a88b..00000000 --- a/contracts/products/bondBTC/BondBtcRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./BondBtcMidasAccessControlRoles.sol"; - -/** - * @title BondBtcRedemptionVaultWithSwapper - * @notice Smart contract that handles bondBTC redemptions - * @author RedDuck Software - */ -contract BondBtcRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - BondBtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return BOND_BTC_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/bondBTC/bondBTC.sol b/contracts/products/bondBTC/bondBTC.sol deleted file mode 100644 index f364f790..00000000 --- a/contracts/products/bondBTC/bondBTC.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title bondBTC - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract bondBTC is mToken { - /** - * @notice actor that can mint bondBTC - */ - bytes32 public constant BOND_BTC_MINT_OPERATOR_ROLE = - keccak256("BOND_BTC_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn bondBTC - */ - bytes32 public constant BOND_BTC_BURN_OPERATOR_ROLE = - keccak256("BOND_BTC_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause bondBTC - */ - bytes32 public constant BOND_BTC_PAUSE_OPERATOR_ROLE = - keccak256("BOND_BTC_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Bond BTC", "bondBTC"); - } - - /** - * @dev AC role, owner of which can mint bondBTC token - */ - function _minterRole() internal pure override returns (bytes32) { - return BOND_BTC_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn bondBTC token - */ - function _burnerRole() internal pure override returns (bytes32) { - return BOND_BTC_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause bondBTC token - */ - function _pauserRole() internal pure override returns (bytes32) { - return BOND_BTC_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/bondETH/BondEthCustomAggregatorFeed.sol b/contracts/products/bondETH/BondEthCustomAggregatorFeed.sol deleted file mode 100644 index 257ae81e..00000000 --- a/contracts/products/bondETH/BondEthCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./BondEthMidasAccessControlRoles.sol"; - -/** - * @title BondEthCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for bondETH, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract BondEthCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - BondEthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return BOND_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/bondETH/BondEthDataFeed.sol b/contracts/products/bondETH/BondEthDataFeed.sol deleted file mode 100644 index 3342ab35..00000000 --- a/contracts/products/bondETH/BondEthDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./BondEthMidasAccessControlRoles.sol"; - -/** - * @title BondEthDataFeed - * @notice DataFeed for bondETH product - * @author RedDuck Software - */ -contract BondEthDataFeed is DataFeed, BondEthMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return BOND_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/bondETH/BondEthDepositVault.sol b/contracts/products/bondETH/BondEthDepositVault.sol deleted file mode 100644 index d406e9bb..00000000 --- a/contracts/products/bondETH/BondEthDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./BondEthMidasAccessControlRoles.sol"; - -/** - * @title BondEthDepositVault - * @notice Smart contract that handles bondETH minting - * @author RedDuck Software - */ -contract BondEthDepositVault is DepositVault, BondEthMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return BOND_ETH_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/bondETH/BondEthMidasAccessControlRoles.sol b/contracts/products/bondETH/BondEthMidasAccessControlRoles.sol deleted file mode 100644 index 475b1e67..00000000 --- a/contracts/products/bondETH/BondEthMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title BondEthMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for bondETH contracts - * @author RedDuck Software - */ -abstract contract BondEthMidasAccessControlRoles { - /** - * @notice actor that can manage BondEthDepositVault - */ - bytes32 public constant BOND_ETH_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("BOND_ETH_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage BondEthRedemptionVault - */ - bytes32 public constant BOND_ETH_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("BOND_ETH_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage BondEthCustomAggregatorFeed and BondEthDataFeed - */ - bytes32 public constant BOND_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("BOND_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/bondETH/BondEthRedemptionVaultWithSwapper.sol b/contracts/products/bondETH/BondEthRedemptionVaultWithSwapper.sol deleted file mode 100644 index 27887087..00000000 --- a/contracts/products/bondETH/BondEthRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./BondEthMidasAccessControlRoles.sol"; - -/** - * @title BondEthRedemptionVaultWithSwapper - * @notice Smart contract that handles bondETH redemptions - * @author RedDuck Software - */ -contract BondEthRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - BondEthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return BOND_ETH_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/bondETH/bondETH.sol b/contracts/products/bondETH/bondETH.sol deleted file mode 100644 index d55df784..00000000 --- a/contracts/products/bondETH/bondETH.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title bondETH - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract bondETH is mToken { - /** - * @notice actor that can mint bondETH - */ - bytes32 public constant BOND_ETH_MINT_OPERATOR_ROLE = - keccak256("BOND_ETH_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn bondETH - */ - bytes32 public constant BOND_ETH_BURN_OPERATOR_ROLE = - keccak256("BOND_ETH_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause bondETH - */ - bytes32 public constant BOND_ETH_PAUSE_OPERATOR_ROLE = - keccak256("BOND_ETH_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Bond ETH", "bondETH"); - } - - /** - * @dev AC role, owner of which can mint bondETH token - */ - function _minterRole() internal pure override returns (bytes32) { - return BOND_ETH_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn bondETH token - */ - function _burnerRole() internal pure override returns (bytes32) { - return BOND_ETH_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause bondETH token - */ - function _pauserRole() internal pure override returns (bytes32) { - return BOND_ETH_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/bondUSD/BondUsdCustomAggregatorFeed.sol b/contracts/products/bondUSD/BondUsdCustomAggregatorFeed.sol deleted file mode 100644 index 781a6d5b..00000000 --- a/contracts/products/bondUSD/BondUsdCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./BondUsdMidasAccessControlRoles.sol"; - -/** - * @title BondUsdCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for bondUSD, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract BondUsdCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - BondUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return BOND_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/bondUSD/BondUsdDataFeed.sol b/contracts/products/bondUSD/BondUsdDataFeed.sol deleted file mode 100644 index aa5ca561..00000000 --- a/contracts/products/bondUSD/BondUsdDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./BondUsdMidasAccessControlRoles.sol"; - -/** - * @title BondUsdDataFeed - * @notice DataFeed for bondUSD product - * @author RedDuck Software - */ -contract BondUsdDataFeed is DataFeed, BondUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return BOND_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/bondUSD/BondUsdDepositVault.sol b/contracts/products/bondUSD/BondUsdDepositVault.sol deleted file mode 100644 index 89d65b55..00000000 --- a/contracts/products/bondUSD/BondUsdDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./BondUsdMidasAccessControlRoles.sol"; - -/** - * @title BondUsdDepositVault - * @notice Smart contract that handles bondUSD minting - * @author RedDuck Software - */ -contract BondUsdDepositVault is DepositVault, BondUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return BOND_USD_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/bondUSD/BondUsdMidasAccessControlRoles.sol b/contracts/products/bondUSD/BondUsdMidasAccessControlRoles.sol deleted file mode 100644 index 6d39dad2..00000000 --- a/contracts/products/bondUSD/BondUsdMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title BondUsdMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for bondUSD contracts - * @author RedDuck Software - */ -abstract contract BondUsdMidasAccessControlRoles { - /** - * @notice actor that can manage BondUsdDepositVault - */ - bytes32 public constant BOND_USD_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("BOND_USD_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage BondUsdRedemptionVault - */ - bytes32 public constant BOND_USD_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("BOND_USD_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage BondUsdCustomAggregatorFeed and BondUsdDataFeed - */ - bytes32 public constant BOND_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("BOND_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/bondUSD/BondUsdRedemptionVaultWithSwapper.sol b/contracts/products/bondUSD/BondUsdRedemptionVaultWithSwapper.sol deleted file mode 100644 index e175561e..00000000 --- a/contracts/products/bondUSD/BondUsdRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./BondUsdMidasAccessControlRoles.sol"; - -/** - * @title BondUsdRedemptionVaultWithSwapper - * @notice Smart contract that handles bondUSD redemptions - * @author RedDuck Software - */ -contract BondUsdRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - BondUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return BOND_USD_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/bondUSD/bondUSD.sol b/contracts/products/bondUSD/bondUSD.sol deleted file mode 100644 index af26b94f..00000000 --- a/contracts/products/bondUSD/bondUSD.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title bondUSD - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract bondUSD is mToken { - /** - * @notice actor that can mint bondUSD - */ - bytes32 public constant BOND_USD_MINT_OPERATOR_ROLE = - keccak256("BOND_USD_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn bondUSD - */ - bytes32 public constant BOND_USD_BURN_OPERATOR_ROLE = - keccak256("BOND_USD_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause bondUSD - */ - bytes32 public constant BOND_USD_PAUSE_OPERATOR_ROLE = - keccak256("BOND_USD_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Bond USD", "bondUSD"); - } - - /** - * @dev AC role, owner of which can mint bondUSD token - */ - function _minterRole() internal pure override returns (bytes32) { - return BOND_USD_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn bondUSD token - */ - function _burnerRole() internal pure override returns (bytes32) { - return BOND_USD_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause bondUSD token - */ - function _pauserRole() internal pure override returns (bytes32) { - return BOND_USD_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/cUSDO/CUsdoCustomAggregatorFeed.sol b/contracts/products/cUSDO/CUsdoCustomAggregatorFeed.sol deleted file mode 100644 index 95f1b30f..00000000 --- a/contracts/products/cUSDO/CUsdoCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./CUsdoMidasAccessControlRoles.sol"; - -/** - * @title CUsdoCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for cUSDO, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract CUsdoCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - CUsdoMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return C_USDO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/cUSDO/CUsdoDataFeed.sol b/contracts/products/cUSDO/CUsdoDataFeed.sol deleted file mode 100644 index 6ea35ed3..00000000 --- a/contracts/products/cUSDO/CUsdoDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./CUsdoMidasAccessControlRoles.sol"; - -/** - * @title CUsdoDataFeed - * @notice DataFeed for cUSDO product - * @author RedDuck Software - */ -contract CUsdoDataFeed is DataFeed, CUsdoMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return C_USDO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/cUSDO/CUsdoDepositVault.sol b/contracts/products/cUSDO/CUsdoDepositVault.sol deleted file mode 100644 index 464d797c..00000000 --- a/contracts/products/cUSDO/CUsdoDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./CUsdoMidasAccessControlRoles.sol"; - -/** - * @title CUsdoDepositVault - * @notice Smart contract that handles cUSDO minting - * @author RedDuck Software - */ -contract CUsdoDepositVault is DepositVault, CUsdoMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return C_USDO_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/cUSDO/CUsdoMidasAccessControlRoles.sol b/contracts/products/cUSDO/CUsdoMidasAccessControlRoles.sol deleted file mode 100644 index 4b01a8c9..00000000 --- a/contracts/products/cUSDO/CUsdoMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title CUsdoMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for cUSDO contracts - * @author RedDuck Software - */ -abstract contract CUsdoMidasAccessControlRoles { - /** - * @notice actor that can manage CUsdoDepositVault - */ - bytes32 public constant C_USDO_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("C_USDO_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage CUsdoRedemptionVault - */ - bytes32 public constant C_USDO_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("C_USDO_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage CUsdoCustomAggregatorFeed and CUsdoDataFeed - */ - bytes32 public constant C_USDO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("C_USDO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/cUSDO/CUsdoRedemptionVaultWithSwapper.sol b/contracts/products/cUSDO/CUsdoRedemptionVaultWithSwapper.sol deleted file mode 100644 index 7f7093c3..00000000 --- a/contracts/products/cUSDO/CUsdoRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./CUsdoMidasAccessControlRoles.sol"; - -/** - * @title CUsdoRedemptionVaultWithSwapper - * @notice Smart contract that handles cUSDO redemptions - * @author RedDuck Software - */ -contract CUsdoRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - CUsdoMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return C_USDO_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/cUSDO/cUSDO.sol b/contracts/products/cUSDO/cUSDO.sol deleted file mode 100644 index fd18219f..00000000 --- a/contracts/products/cUSDO/cUSDO.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title cUSDO - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract cUSDO is mToken { - /** - * @notice actor that can mint cUSDO - */ - bytes32 public constant C_USDO_MINT_OPERATOR_ROLE = - keccak256("C_USDO_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn cUSDO - */ - bytes32 public constant C_USDO_BURN_OPERATOR_ROLE = - keccak256("C_USDO_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause cUSDO - */ - bytes32 public constant C_USDO_PAUSE_OPERATOR_ROLE = - keccak256("C_USDO_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("cUSDO BNB Midas Vault", "cUSDO"); - } - - /** - * @dev AC role, owner of which can mint cUSDO token - */ - function _minterRole() internal pure override returns (bytes32) { - return C_USDO_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn cUSDO token - */ - function _burnerRole() internal pure override returns (bytes32) { - return C_USDO_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause cUSDO token - */ - function _pauserRole() internal pure override returns (bytes32) { - return C_USDO_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageCustomAggregatorFeed.sol b/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageCustomAggregatorFeed.sol deleted file mode 100644 index b1fd50a0..00000000 --- a/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./CarryTradeUsdTryLeverageMidasAccessControlRoles.sol"; - -/** - * @title CarryTradeUsdTryLeverageCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for carryTradeUSDTRYLeverage, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract CarryTradeUsdTryLeverageCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - CarryTradeUsdTryLeverageMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return CARRY_TRADE_USD_TRY_LEVERAGE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageDataFeed.sol b/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageDataFeed.sol deleted file mode 100644 index 2f265c08..00000000 --- a/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageDataFeed.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./CarryTradeUsdTryLeverageMidasAccessControlRoles.sol"; - -/** - * @title CarryTradeUsdTryLeverageDataFeed - * @notice DataFeed for carryTradeUSDTRYLeverage product - * @author RedDuck Software - */ -contract CarryTradeUsdTryLeverageDataFeed is - DataFeed, - CarryTradeUsdTryLeverageMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return CARRY_TRADE_USD_TRY_LEVERAGE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageDepositVault.sol b/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageDepositVault.sol deleted file mode 100644 index b2b7b4e4..00000000 --- a/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./CarryTradeUsdTryLeverageMidasAccessControlRoles.sol"; - -/** - * @title CarryTradeUsdTryLeverageDepositVault - * @notice Smart contract that handles carryTradeUSDTRYLeverage minting - * @author RedDuck Software - */ -contract CarryTradeUsdTryLeverageDepositVault is - DepositVault, - CarryTradeUsdTryLeverageMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return CARRY_TRADE_USD_TRY_LEVERAGE_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageMidasAccessControlRoles.sol b/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageMidasAccessControlRoles.sol deleted file mode 100644 index 65befb2d..00000000 --- a/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageMidasAccessControlRoles.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title CarryTradeUsdTryLeverageMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for carryTradeUSDTRYLeverage contracts - * @author RedDuck Software - */ -abstract contract CarryTradeUsdTryLeverageMidasAccessControlRoles { - /** - * @notice actor that can manage CarryTradeUsdTryLeverageDepositVault - */ - bytes32 - public constant CARRY_TRADE_USD_TRY_LEVERAGE_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("CARRY_TRADE_USD_TRY_LEVERAGE_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage CarryTradeUsdTryLeverageRedemptionVault - */ - bytes32 - public constant CARRY_TRADE_USD_TRY_LEVERAGE_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("CARRY_TRADE_USD_TRY_LEVERAGE_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage CarryTradeUsdTryLeverageCustomAggregatorFeed and CarryTradeUsdTryLeverageDataFeed - */ - bytes32 - public constant CARRY_TRADE_USD_TRY_LEVERAGE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256( - "CARRY_TRADE_USD_TRY_LEVERAGE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE" - ); -} diff --git a/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageRedemptionVaultWithSwapper.sol b/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageRedemptionVaultWithSwapper.sol deleted file mode 100644 index fe025f9f..00000000 --- a/contracts/products/carryTradeUSDTRYLeverage/CarryTradeUsdTryLeverageRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./CarryTradeUsdTryLeverageMidasAccessControlRoles.sol"; - -/** - * @title CarryTradeUsdTryLeverageRedemptionVaultWithSwapper - * @notice Smart contract that handles carryTradeUSDTRYLeverage redemptions - * @author RedDuck Software - */ -contract CarryTradeUsdTryLeverageRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - CarryTradeUsdTryLeverageMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return CARRY_TRADE_USD_TRY_LEVERAGE_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/carryTradeUSDTRYLeverage/carryTradeUSDTRYLeverage.sol b/contracts/products/carryTradeUSDTRYLeverage/carryTradeUSDTRYLeverage.sol deleted file mode 100644 index 3a916f92..00000000 --- a/contracts/products/carryTradeUSDTRYLeverage/carryTradeUSDTRYLeverage.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title carryTradeUSDTRYLeverage - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract carryTradeUSDTRYLeverage is mToken { - /** - * @notice actor that can mint carryTradeUSDTRYLeverage - */ - bytes32 public constant CARRY_TRADE_USD_TRY_LEVERAGE_MINT_OPERATOR_ROLE = - keccak256("CARRY_TRADE_USD_TRY_LEVERAGE_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn carryTradeUSDTRYLeverage - */ - bytes32 public constant CARRY_TRADE_USD_TRY_LEVERAGE_BURN_OPERATOR_ROLE = - keccak256("CARRY_TRADE_USD_TRY_LEVERAGE_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause carryTradeUSDTRYLeverage - */ - bytes32 public constant CARRY_TRADE_USD_TRY_LEVERAGE_PAUSE_OPERATOR_ROLE = - keccak256("CARRY_TRADE_USD_TRY_LEVERAGE_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ( - "Morini CarryTradeUSDTRYLeverage Vault", - "CarryTradeUSDTRYLeverage" - ); - } - - /** - * @dev AC role, owner of which can mint carryTradeUSDTRYLeverage token - */ - function _minterRole() internal pure override returns (bytes32) { - return CARRY_TRADE_USD_TRY_LEVERAGE_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn carryTradeUSDTRYLeverage token - */ - function _burnerRole() internal pure override returns (bytes32) { - return CARRY_TRADE_USD_TRY_LEVERAGE_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause carryTradeUSDTRYLeverage token - */ - function _pauserRole() internal pure override returns (bytes32) { - return CARRY_TRADE_USD_TRY_LEVERAGE_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/dnETH/DnEthCustomAggregatorFeed.sol b/contracts/products/dnETH/DnEthCustomAggregatorFeed.sol deleted file mode 100644 index a56cb3a0..00000000 --- a/contracts/products/dnETH/DnEthCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./DnEthMidasAccessControlRoles.sol"; - -/** - * @title DnEthCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for dnETH, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract DnEthCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - DnEthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return DN_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnETH/DnEthDataFeed.sol b/contracts/products/dnETH/DnEthDataFeed.sol deleted file mode 100644 index ef345574..00000000 --- a/contracts/products/dnETH/DnEthDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./DnEthMidasAccessControlRoles.sol"; - -/** - * @title DnEthDataFeed - * @notice DataFeed for dnETH product - * @author RedDuck Software - */ -contract DnEthDataFeed is DataFeed, DnEthMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return DN_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnETH/DnEthDepositVault.sol b/contracts/products/dnETH/DnEthDepositVault.sol deleted file mode 100644 index 27aeda91..00000000 --- a/contracts/products/dnETH/DnEthDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./DnEthMidasAccessControlRoles.sol"; - -/** - * @title DnEthDepositVault - * @notice Smart contract that handles dnETH minting - * @author RedDuck Software - */ -contract DnEthDepositVault is DepositVault, DnEthMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return DN_ETH_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnETH/DnEthMidasAccessControlRoles.sol b/contracts/products/dnETH/DnEthMidasAccessControlRoles.sol deleted file mode 100644 index 49c870cd..00000000 --- a/contracts/products/dnETH/DnEthMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title DnEthMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for dnETH contracts - * @author RedDuck Software - */ -abstract contract DnEthMidasAccessControlRoles { - /** - * @notice actor that can manage DnEthDepositVault - */ - bytes32 public constant DN_ETH_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("DN_ETH_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage DnEthRedemptionVault - */ - bytes32 public constant DN_ETH_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("DN_ETH_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage DnEthCustomAggregatorFeed and DnEthDataFeed - */ - bytes32 public constant DN_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("DN_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/dnETH/DnEthRedemptionVaultWithSwapper.sol b/contracts/products/dnETH/DnEthRedemptionVaultWithSwapper.sol deleted file mode 100644 index d697772e..00000000 --- a/contracts/products/dnETH/DnEthRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./DnEthMidasAccessControlRoles.sol"; - -/** - * @title DnEthRedemptionVaultWithSwapper - * @notice Smart contract that handles dnETH redemptions - * @author RedDuck Software - */ -contract DnEthRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - DnEthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return DN_ETH_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnETH/dnETH.sol b/contracts/products/dnETH/dnETH.sol deleted file mode 100644 index a2fb1f76..00000000 --- a/contracts/products/dnETH/dnETH.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title dnETH - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract dnETH is mToken { - /** - * @notice actor that can mint dnETH - */ - bytes32 public constant DN_ETH_MINT_OPERATOR_ROLE = - keccak256("DN_ETH_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn dnETH - */ - bytes32 public constant DN_ETH_BURN_OPERATOR_ROLE = - keccak256("DN_ETH_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause dnETH - */ - bytes32 public constant DN_ETH_PAUSE_OPERATOR_ROLE = - keccak256("DN_ETH_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Delta Neutral ETH", "dnETH"); - } - - /** - * @dev AC role, owner of which can mint dnETH token - */ - function _minterRole() internal pure override returns (bytes32) { - return DN_ETH_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn dnETH token - */ - function _burnerRole() internal pure override returns (bytes32) { - return DN_ETH_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause dnETH token - */ - function _pauserRole() internal pure override returns (bytes32) { - return DN_ETH_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/dnFART/DnFartCustomAggregatorFeed.sol b/contracts/products/dnFART/DnFartCustomAggregatorFeed.sol deleted file mode 100644 index 32a337af..00000000 --- a/contracts/products/dnFART/DnFartCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./DnFartMidasAccessControlRoles.sol"; - -/** - * @title DnFartCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for dnFART, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract DnFartCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - DnFartMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return DN_FART_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnFART/DnFartDataFeed.sol b/contracts/products/dnFART/DnFartDataFeed.sol deleted file mode 100644 index 9cb7220d..00000000 --- a/contracts/products/dnFART/DnFartDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./DnFartMidasAccessControlRoles.sol"; - -/** - * @title DnFartDataFeed - * @notice DataFeed for dnFART product - * @author RedDuck Software - */ -contract DnFartDataFeed is DataFeed, DnFartMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return DN_FART_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnFART/DnFartDepositVault.sol b/contracts/products/dnFART/DnFartDepositVault.sol deleted file mode 100644 index dc34cff9..00000000 --- a/contracts/products/dnFART/DnFartDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./DnFartMidasAccessControlRoles.sol"; - -/** - * @title DnFartDepositVault - * @notice Smart contract that handles dnFART minting - * @author RedDuck Software - */ -contract DnFartDepositVault is DepositVault, DnFartMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return DN_FART_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnFART/DnFartMidasAccessControlRoles.sol b/contracts/products/dnFART/DnFartMidasAccessControlRoles.sol deleted file mode 100644 index ef1a1c9e..00000000 --- a/contracts/products/dnFART/DnFartMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title DnFartMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for dnFART contracts - * @author RedDuck Software - */ -abstract contract DnFartMidasAccessControlRoles { - /** - * @notice actor that can manage DnFartDepositVault - */ - bytes32 public constant DN_FART_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("DN_FART_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage DnFartRedemptionVault - */ - bytes32 public constant DN_FART_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("DN_FART_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage DnFartCustomAggregatorFeed and DnFartDataFeed - */ - bytes32 public constant DN_FART_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("DN_FART_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/dnFART/DnFartRedemptionVaultWithSwapper.sol b/contracts/products/dnFART/DnFartRedemptionVaultWithSwapper.sol deleted file mode 100644 index c7646182..00000000 --- a/contracts/products/dnFART/DnFartRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./DnFartMidasAccessControlRoles.sol"; - -/** - * @title DnFartRedemptionVaultWithSwapper - * @notice Smart contract that handles dnFART redemptions - * @author RedDuck Software - */ -contract DnFartRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - DnFartMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return DN_FART_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnFART/dnFART.sol b/contracts/products/dnFART/dnFART.sol deleted file mode 100644 index ebddd908..00000000 --- a/contracts/products/dnFART/dnFART.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title dnFART - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract dnFART is mToken { - /** - * @notice actor that can mint dnFART - */ - bytes32 public constant DN_FART_MINT_OPERATOR_ROLE = - keccak256("DN_FART_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn dnFART - */ - bytes32 public constant DN_FART_BURN_OPERATOR_ROLE = - keccak256("DN_FART_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause dnFART - */ - bytes32 public constant DN_FART_PAUSE_OPERATOR_ROLE = - keccak256("DN_FART_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Delta Neutral FART", "dnFART"); - } - - /** - * @dev AC role, owner of which can mint dnFART token - */ - function _minterRole() internal pure override returns (bytes32) { - return DN_FART_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn dnFART token - */ - function _burnerRole() internal pure override returns (bytes32) { - return DN_FART_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause dnFART token - */ - function _pauserRole() internal pure override returns (bytes32) { - return DN_FART_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/dnHYPE/DnHypeCustomAggregatorFeed.sol b/contracts/products/dnHYPE/DnHypeCustomAggregatorFeed.sol deleted file mode 100644 index 214db6ba..00000000 --- a/contracts/products/dnHYPE/DnHypeCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./DnHypeMidasAccessControlRoles.sol"; - -/** - * @title DnHypeCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for dnHYPE, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract DnHypeCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - DnHypeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return DN_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnHYPE/DnHypeDataFeed.sol b/contracts/products/dnHYPE/DnHypeDataFeed.sol deleted file mode 100644 index 074cb258..00000000 --- a/contracts/products/dnHYPE/DnHypeDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./DnHypeMidasAccessControlRoles.sol"; - -/** - * @title DnHypeDataFeed - * @notice DataFeed for dnHYPE product - * @author RedDuck Software - */ -contract DnHypeDataFeed is DataFeed, DnHypeMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return DN_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnHYPE/DnHypeDepositVault.sol b/contracts/products/dnHYPE/DnHypeDepositVault.sol deleted file mode 100644 index ea269523..00000000 --- a/contracts/products/dnHYPE/DnHypeDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./DnHypeMidasAccessControlRoles.sol"; - -/** - * @title DnHypeDepositVault - * @notice Smart contract that handles dnHYPE minting - * @author RedDuck Software - */ -contract DnHypeDepositVault is DepositVault, DnHypeMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return DN_HYPE_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnHYPE/DnHypeMidasAccessControlRoles.sol b/contracts/products/dnHYPE/DnHypeMidasAccessControlRoles.sol deleted file mode 100644 index 25fc4138..00000000 --- a/contracts/products/dnHYPE/DnHypeMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title DnHypeMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for dnHYPE contracts - * @author RedDuck Software - */ -abstract contract DnHypeMidasAccessControlRoles { - /** - * @notice actor that can manage DnHypeDepositVault - */ - bytes32 public constant DN_HYPE_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("DN_HYPE_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage DnHypeRedemptionVault - */ - bytes32 public constant DN_HYPE_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("DN_HYPE_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage DnHypeCustomAggregatorFeed and DnHypeDataFeed - */ - bytes32 public constant DN_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("DN_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/dnHYPE/DnHypeRedemptionVaultWithSwapper.sol b/contracts/products/dnHYPE/DnHypeRedemptionVaultWithSwapper.sol deleted file mode 100644 index 03112a5d..00000000 --- a/contracts/products/dnHYPE/DnHypeRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./DnHypeMidasAccessControlRoles.sol"; - -/** - * @title DnHypeRedemptionVaultWithSwapper - * @notice Smart contract that handles dnHYPE redemptions - * @author RedDuck Software - */ -contract DnHypeRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - DnHypeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return DN_HYPE_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnHYPE/dnHYPE.sol b/contracts/products/dnHYPE/dnHYPE.sol deleted file mode 100644 index e33ecdf4..00000000 --- a/contracts/products/dnHYPE/dnHYPE.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title dnHYPE - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract dnHYPE is mToken { - /** - * @notice actor that can mint dnHYPE - */ - bytes32 public constant DN_HYPE_MINT_OPERATOR_ROLE = - keccak256("DN_HYPE_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn dnHYPE - */ - bytes32 public constant DN_HYPE_BURN_OPERATOR_ROLE = - keccak256("DN_HYPE_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause dnHYPE - */ - bytes32 public constant DN_HYPE_PAUSE_OPERATOR_ROLE = - keccak256("DN_HYPE_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Delta Neutral HYPE", "dnHYPE"); - } - - /** - * @dev AC role, owner of which can mint dnHYPE token - */ - function _minterRole() internal pure override returns (bytes32) { - return DN_HYPE_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn dnHYPE token - */ - function _burnerRole() internal pure override returns (bytes32) { - return DN_HYPE_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause dnHYPE token - */ - function _pauserRole() internal pure override returns (bytes32) { - return DN_HYPE_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/dnPUMP/DnPumpCustomAggregatorFeed.sol b/contracts/products/dnPUMP/DnPumpCustomAggregatorFeed.sol deleted file mode 100644 index fd827045..00000000 --- a/contracts/products/dnPUMP/DnPumpCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./DnPumpMidasAccessControlRoles.sol"; - -/** - * @title DnPumpCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for dnPUMP, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract DnPumpCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - DnPumpMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return DN_PUMP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnPUMP/DnPumpDataFeed.sol b/contracts/products/dnPUMP/DnPumpDataFeed.sol deleted file mode 100644 index e87b12cf..00000000 --- a/contracts/products/dnPUMP/DnPumpDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./DnPumpMidasAccessControlRoles.sol"; - -/** - * @title DnPumpDataFeed - * @notice DataFeed for dnPUMP product - * @author RedDuck Software - */ -contract DnPumpDataFeed is DataFeed, DnPumpMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return DN_PUMP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnPUMP/DnPumpDepositVault.sol b/contracts/products/dnPUMP/DnPumpDepositVault.sol deleted file mode 100644 index b1e9fd57..00000000 --- a/contracts/products/dnPUMP/DnPumpDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./DnPumpMidasAccessControlRoles.sol"; - -/** - * @title DnPumpDepositVault - * @notice Smart contract that handles dnPUMP minting - * @author RedDuck Software - */ -contract DnPumpDepositVault is DepositVault, DnPumpMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return DN_PUMP_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnPUMP/DnPumpMidasAccessControlRoles.sol b/contracts/products/dnPUMP/DnPumpMidasAccessControlRoles.sol deleted file mode 100644 index 20f32775..00000000 --- a/contracts/products/dnPUMP/DnPumpMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title DnPumpMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for dnPUMP contracts - * @author RedDuck Software - */ -abstract contract DnPumpMidasAccessControlRoles { - /** - * @notice actor that can manage DnPumpDepositVault - */ - bytes32 public constant DN_PUMP_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("DN_PUMP_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage DnPumpRedemptionVault - */ - bytes32 public constant DN_PUMP_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("DN_PUMP_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage DnPumpCustomAggregatorFeed and DnPumpDataFeed - */ - bytes32 public constant DN_PUMP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("DN_PUMP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/dnPUMP/DnPumpRedemptionVaultWithSwapper.sol b/contracts/products/dnPUMP/DnPumpRedemptionVaultWithSwapper.sol deleted file mode 100644 index 23d14c31..00000000 --- a/contracts/products/dnPUMP/DnPumpRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./DnPumpMidasAccessControlRoles.sol"; - -/** - * @title DnPumpRedemptionVaultWithSwapper - * @notice Smart contract that handles dnPUMP redemptions - * @author RedDuck Software - */ -contract DnPumpRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - DnPumpMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return DN_PUMP_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnPUMP/dnPUMP.sol b/contracts/products/dnPUMP/dnPUMP.sol deleted file mode 100644 index 21b8c2e2..00000000 --- a/contracts/products/dnPUMP/dnPUMP.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title dnPUMP - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract dnPUMP is mToken { - /** - * @notice actor that can mint dnPUMP - */ - bytes32 public constant DN_PUMP_MINT_OPERATOR_ROLE = - keccak256("DN_PUMP_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn dnPUMP - */ - bytes32 public constant DN_PUMP_BURN_OPERATOR_ROLE = - keccak256("DN_PUMP_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause dnPUMP - */ - bytes32 public constant DN_PUMP_PAUSE_OPERATOR_ROLE = - keccak256("DN_PUMP_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Delta Neutral PUMP", "dnPUMP"); - } - - /** - * @dev AC role, owner of which can mint dnPUMP token - */ - function _minterRole() internal pure override returns (bytes32) { - return DN_PUMP_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn dnPUMP token - */ - function _burnerRole() internal pure override returns (bytes32) { - return DN_PUMP_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause dnPUMP token - */ - function _pauserRole() internal pure override returns (bytes32) { - return DN_PUMP_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/dnTEST/DnTestCustomAggregatorFeedGrowth.sol b/contracts/products/dnTEST/DnTestCustomAggregatorFeedGrowth.sol deleted file mode 100644 index a2956648..00000000 --- a/contracts/products/dnTEST/DnTestCustomAggregatorFeedGrowth.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeedGrowth.sol"; -import "./DnTestMidasAccessControlRoles.sol"; - -/** - * @title DnTestCustomAggregatorFeedGrowth - * @notice AggregatorV3 compatible feed for dnTEST, - * where price is submitted manually by feed admins, - * and growth apr applies to the answer. - * @author RedDuck Software - */ -contract DnTestCustomAggregatorFeedGrowth is - CustomAggregatorV3CompatibleFeedGrowth, - DnTestMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeedGrowth - */ - function feedAdminRole() public pure override returns (bytes32) { - return DN_TEST_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnTEST/DnTestDataFeed.sol b/contracts/products/dnTEST/DnTestDataFeed.sol deleted file mode 100644 index 4c7cb45d..00000000 --- a/contracts/products/dnTEST/DnTestDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./DnTestMidasAccessControlRoles.sol"; - -/** - * @title DnTestDataFeed - * @notice DataFeed for dnTEST product - * @author RedDuck Software - */ -contract DnTestDataFeed is DataFeed, DnTestMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return DN_TEST_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnTEST/DnTestDepositVault.sol b/contracts/products/dnTEST/DnTestDepositVault.sol deleted file mode 100644 index 8326973c..00000000 --- a/contracts/products/dnTEST/DnTestDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./DnTestMidasAccessControlRoles.sol"; - -/** - * @title DnTestDepositVault - * @notice Smart contract that handles dnTEST minting - * @author RedDuck Software - */ -contract DnTestDepositVault is DepositVault, DnTestMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return DN_TEST_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnTEST/DnTestMidasAccessControlRoles.sol b/contracts/products/dnTEST/DnTestMidasAccessControlRoles.sol deleted file mode 100644 index 272269af..00000000 --- a/contracts/products/dnTEST/DnTestMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title DnTestMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for dnTEST contracts - * @author RedDuck Software - */ -abstract contract DnTestMidasAccessControlRoles { - /** - * @notice actor that can manage DnTestDepositVault - */ - bytes32 public constant DN_TEST_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("DN_TEST_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage DnTestRedemptionVault - */ - bytes32 public constant DN_TEST_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("DN_TEST_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage DnTestCustomAggregatorFeed and DnTestDataFeed - */ - bytes32 public constant DN_TEST_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("DN_TEST_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/dnTEST/DnTestRedemptionVaultWithSwapper.sol b/contracts/products/dnTEST/DnTestRedemptionVaultWithSwapper.sol deleted file mode 100644 index ca7e2722..00000000 --- a/contracts/products/dnTEST/DnTestRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./DnTestMidasAccessControlRoles.sol"; - -/** - * @title DnTestRedemptionVaultWithSwapper - * @notice Smart contract that handles dnTEST redemptions - * @author RedDuck Software - */ -contract DnTestRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - DnTestMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return DN_TEST_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/dnTEST/dnTEST.sol b/contracts/products/dnTEST/dnTEST.sol deleted file mode 100644 index 13478685..00000000 --- a/contracts/products/dnTEST/dnTEST.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title dnTEST - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract dnTEST is mToken { - /** - * @notice actor that can mint dnTEST - */ - bytes32 public constant DN_TEST_MINT_OPERATOR_ROLE = - keccak256("DN_TEST_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn dnTEST - */ - bytes32 public constant DN_TEST_BURN_OPERATOR_ROLE = - keccak256("DN_TEST_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause dnTEST - */ - bytes32 public constant DN_TEST_PAUSE_OPERATOR_ROLE = - keccak256("DN_TEST_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Delta Neutral TEST", "dnTEST"); - } - - /** - * @dev AC role, owner of which can mint dnTEST token - */ - function _minterRole() internal pure override returns (bytes32) { - return DN_TEST_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn dnTEST token - */ - function _burnerRole() internal pure override returns (bytes32) { - return DN_TEST_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause dnTEST token - */ - function _pauserRole() internal pure override returns (bytes32) { - return DN_TEST_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/eUSD/EUsdDepositVault.sol b/contracts/products/eUSD/EUsdDepositVault.sol deleted file mode 100644 index 2a9a76c6..00000000 --- a/contracts/products/eUSD/EUsdDepositVault.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "./EUsdMidasAccessControlRoles.sol"; -import "../../DepositVault.sol"; - -/** - * @title EUsdDepositVault - * @notice Smart contract that handles eUSD minting - * @author RedDuck Software - */ -contract EUsdDepositVault is DepositVault, EUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return E_USD_DEPOSIT_VAULT_ADMIN_ROLE; - } - - function greenlistedRole() public pure override returns (bytes32) { - return E_USD_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/eUSD/EUsdMidasAccessControlRoles.sol b/contracts/products/eUSD/EUsdMidasAccessControlRoles.sol deleted file mode 100644 index bbb28a76..00000000 --- a/contracts/products/eUSD/EUsdMidasAccessControlRoles.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title EUsdMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for eUSD contracts - * @author RedDuck Software - */ -abstract contract EUsdMidasAccessControlRoles { - /** - * @notice actor that can manage vault admin roles - */ - bytes32 public constant E_USD_VAULT_ROLES_OPERATOR = - keccak256("E_USD_VAULT_ROLES_OPERATOR"); - - /** - * @notice actor that can manage EUsdDepositVault - */ - bytes32 public constant E_USD_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("E_USD_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage EUsdRedemptionVault - */ - bytes32 public constant E_USD_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("E_USD_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can change eUSD green list statuses of addresses - */ - bytes32 public constant E_USD_GREENLIST_OPERATOR_ROLE = - keccak256("E_USD_GREENLIST_OPERATOR_ROLE"); - - /** - * @notice actor that is greenlisted in eUSD - */ - bytes32 public constant E_USD_GREENLISTED_ROLE = - keccak256("E_USD_GREENLISTED_ROLE"); -} diff --git a/contracts/products/eUSD/EUsdRedemptionVault.sol b/contracts/products/eUSD/EUsdRedemptionVault.sol deleted file mode 100644 index 918178ba..00000000 --- a/contracts/products/eUSD/EUsdRedemptionVault.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "./EUsdMidasAccessControlRoles.sol"; -import "../../RedemptionVault.sol"; - -/** - * @title EUsdRedemptionVault - * @notice Smart contract that handles eUSD redeeming - * @author RedDuck Software - */ -contract EUsdRedemptionVault is RedemptionVault, EUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return E_USD_REDEMPTION_VAULT_ADMIN_ROLE; - } - - function greenlistedRole() public pure override returns (bytes32) { - return E_USD_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/eUSD/EUsdRedemptionVaultWithBUIDL.sol b/contracts/products/eUSD/EUsdRedemptionVaultWithBUIDL.sol deleted file mode 100644 index 4c1047f4..00000000 --- a/contracts/products/eUSD/EUsdRedemptionVaultWithBUIDL.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "./EUsdMidasAccessControlRoles.sol"; -import "../../RedemptionVaultWithBUIDL.sol"; - -/** - * @title EUsdRedemptionVaultWithBUIDL - * @notice Smart contract that handles eUSD redeeming - * @author RedDuck Software - */ -contract EUsdRedemptionVaultWithBUIDL is - RedemptionVaultWIthBUIDL, - EUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return E_USD_REDEMPTION_VAULT_ADMIN_ROLE; - } - - function greenlistedRole() public pure override returns (bytes32) { - return E_USD_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/eUSD/eUSD.sol b/contracts/products/eUSD/eUSD.sol deleted file mode 100644 index f92800d8..00000000 --- a/contracts/products/eUSD/eUSD.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title eUSD - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract eUSD is mToken { - /** - * @notice actor that can mint eUSD - */ - bytes32 public constant E_USD_MINT_OPERATOR_ROLE = - keccak256("E_USD_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn eUSD - */ - bytes32 public constant E_USD_BURN_OPERATOR_ROLE = - keccak256("E_USD_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause eUSD - */ - bytes32 public constant E_USD_PAUSE_OPERATOR_ROLE = - keccak256("E_USD_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Eternal USD", "eUSD"); - } - - /** - * @dev AC role, owner of which can mint eUSD token - */ - function _minterRole() internal pure override returns (bytes32) { - return E_USD_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn eUSD token - */ - function _burnerRole() internal pure override returns (bytes32) { - return E_USD_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause eUSD token - */ - function _pauserRole() internal pure override returns (bytes32) { - return E_USD_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/hbUSDC/HBUsdcCustomAggregatorFeed.sol b/contracts/products/hbUSDC/HBUsdcCustomAggregatorFeed.sol deleted file mode 100644 index b38931d7..00000000 --- a/contracts/products/hbUSDC/HBUsdcCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./HBUsdcMidasAccessControlRoles.sol"; - -/** - * @title HBUsdcCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for hbUSDC, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract HBUsdcCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - HBUsdcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return HB_USDC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/hbUSDC/HBUsdcDataFeed.sol b/contracts/products/hbUSDC/HBUsdcDataFeed.sol deleted file mode 100644 index fc1b25e9..00000000 --- a/contracts/products/hbUSDC/HBUsdcDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./HBUsdcMidasAccessControlRoles.sol"; - -/** - * @title HBUsdcDataFeed - * @notice DataFeed for hbUSDC product - * @author RedDuck Software - */ -contract HBUsdcDataFeed is DataFeed, HBUsdcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return HB_USDC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/hbUSDC/HBUsdcDepositVault.sol b/contracts/products/hbUSDC/HBUsdcDepositVault.sol deleted file mode 100644 index 2f7bc8e1..00000000 --- a/contracts/products/hbUSDC/HBUsdcDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./HBUsdcMidasAccessControlRoles.sol"; - -/** - * @title HBUsdcDepositVault - * @notice Smart contract that handles hbUSDC minting - * @author RedDuck Software - */ -contract HBUsdcDepositVault is DepositVault, HBUsdcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return HB_USDC_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/hbUSDC/HBUsdcMidasAccessControlRoles.sol b/contracts/products/hbUSDC/HBUsdcMidasAccessControlRoles.sol deleted file mode 100644 index 13ef3707..00000000 --- a/contracts/products/hbUSDC/HBUsdcMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title HBUsdcMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for hbUSDC contracts - * @author RedDuck Software - */ -abstract contract HBUsdcMidasAccessControlRoles { - /** - * @notice actor that can manage HBUsdcDepositVault - */ - bytes32 public constant HB_USDC_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("HB_USDC_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage HBUsdcRedemptionVault - */ - bytes32 public constant HB_USDC_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("HB_USDC_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage HBUsdcCustomAggregatorFeed and HBUsdcDataFeed - */ - bytes32 public constant HB_USDC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("HB_USDC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/hbUSDC/HBUsdcRedemptionVaultWithSwapper.sol b/contracts/products/hbUSDC/HBUsdcRedemptionVaultWithSwapper.sol deleted file mode 100644 index db38d856..00000000 --- a/contracts/products/hbUSDC/HBUsdcRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./HBUsdcMidasAccessControlRoles.sol"; - -/** - * @title HBUsdcRedemptionVaultWithSwapper - * @notice Smart contract that handles hbUSDC redemptions - * @author RedDuck Software - */ -contract HBUsdcRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - HBUsdcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return HB_USDC_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/hbUSDC/hbUSDC.sol b/contracts/products/hbUSDC/hbUSDC.sol deleted file mode 100644 index 4e73862d..00000000 --- a/contracts/products/hbUSDC/hbUSDC.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title hbUSDC - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract hbUSDC is mToken { - /** - * @notice actor that can mint hbUSDC - */ - bytes32 public constant HB_USDC_MINT_OPERATOR_ROLE = - keccak256("HB_USDC_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn hbUSDC - */ - bytes32 public constant HB_USDC_BURN_OPERATOR_ROLE = - keccak256("HB_USDC_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause hbUSDC - */ - bytes32 public constant HB_USDC_PAUSE_OPERATOR_ROLE = - keccak256("HB_USDC_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Hyperbeat USDC", "hbUSDC"); - } - - /** - * @dev AC role, owner of which can mint hbUSDC token - */ - function _minterRole() internal pure override returns (bytes32) { - return HB_USDC_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn hbUSDC token - */ - function _burnerRole() internal pure override returns (bytes32) { - return HB_USDC_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause hbUSDC token - */ - function _pauserRole() internal pure override returns (bytes32) { - return HB_USDC_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/hbUSDT/HBUsdtCustomAggregatorFeed.sol b/contracts/products/hbUSDT/HBUsdtCustomAggregatorFeed.sol deleted file mode 100644 index 1daea88c..00000000 --- a/contracts/products/hbUSDT/HBUsdtCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./HBUsdtMidasAccessControlRoles.sol"; - -/** - * @title HBUsdtCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for hbUSDT, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract HBUsdtCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - HBUsdtMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return HB_USDT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/hbUSDT/HBUsdtDataFeed.sol b/contracts/products/hbUSDT/HBUsdtDataFeed.sol deleted file mode 100644 index 54a1124c..00000000 --- a/contracts/products/hbUSDT/HBUsdtDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./HBUsdtMidasAccessControlRoles.sol"; - -/** - * @title HBUsdtDataFeed - * @notice DataFeed for hbUSDT product - * @author RedDuck Software - */ -contract HBUsdtDataFeed is DataFeed, HBUsdtMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return HB_USDT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/hbUSDT/HBUsdtDepositVault.sol b/contracts/products/hbUSDT/HBUsdtDepositVault.sol deleted file mode 100644 index e00a3dcd..00000000 --- a/contracts/products/hbUSDT/HBUsdtDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./HBUsdtMidasAccessControlRoles.sol"; - -/** - * @title HBUsdtDepositVault - * @notice Smart contract that handles hbUSDT minting - * @author RedDuck Software - */ -contract HBUsdtDepositVault is DepositVault, HBUsdtMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return HB_USDT_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/hbUSDT/HBUsdtMidasAccessControlRoles.sol b/contracts/products/hbUSDT/HBUsdtMidasAccessControlRoles.sol deleted file mode 100644 index a6934fba..00000000 --- a/contracts/products/hbUSDT/HBUsdtMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title HBUsdtMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for hbUSDT contracts - * @author RedDuck Software - */ -abstract contract HBUsdtMidasAccessControlRoles { - /** - * @notice actor that can manage HBUsdtDepositVault - */ - bytes32 public constant HB_USDT_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("HB_USDT_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage HBUsdtRedemptionVault - */ - bytes32 public constant HB_USDT_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("HB_USDT_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage HBUsdtCustomAggregatorFeed and HBUsdtDataFeed - */ - bytes32 public constant HB_USDT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("HB_USDT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/hbUSDT/HBUsdtRedemptionVaultWithSwapper.sol b/contracts/products/hbUSDT/HBUsdtRedemptionVaultWithSwapper.sol deleted file mode 100644 index 8ada6a0b..00000000 --- a/contracts/products/hbUSDT/HBUsdtRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./HBUsdtMidasAccessControlRoles.sol"; - -/** - * @title HBUsdtRedemptionVaultWithSwapper - * @notice Smart contract that handles hbUSDT redemptions - * @author RedDuck Software - */ -contract HBUsdtRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - HBUsdtMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return HB_USDT_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/hbUSDT/hbUSDT.sol b/contracts/products/hbUSDT/hbUSDT.sol deleted file mode 100644 index 77956ed8..00000000 --- a/contracts/products/hbUSDT/hbUSDT.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title hbUSDT - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract hbUSDT is mToken { - /** - * @notice actor that can mint hbUSDT - */ - bytes32 public constant HB_USDT_MINT_OPERATOR_ROLE = - keccak256("HB_USDT_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn hbUSDT - */ - bytes32 public constant HB_USDT_BURN_OPERATOR_ROLE = - keccak256("HB_USDT_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause hbUSDT - */ - bytes32 public constant HB_USDT_PAUSE_OPERATOR_ROLE = - keccak256("HB_USDT_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Hyperbeat USDT", "hbUSDT"); - } - - /** - * @dev AC role, owner of which can mint hbUSDT token - */ - function _minterRole() internal pure override returns (bytes32) { - return HB_USDT_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn hbUSDT token - */ - function _burnerRole() internal pure override returns (bytes32) { - return HB_USDT_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause hbUSDT token - */ - function _pauserRole() internal pure override returns (bytes32) { - return HB_USDT_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/hbXAUt/HBXautCustomAggregatorFeed.sol b/contracts/products/hbXAUt/HBXautCustomAggregatorFeed.sol deleted file mode 100644 index 19f4ce29..00000000 --- a/contracts/products/hbXAUt/HBXautCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./HBXautMidasAccessControlRoles.sol"; - -/** - * @title HBXautCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for hbXAUt, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract HBXautCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - HBXautMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return HB_XAUT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/hbXAUt/HBXautDataFeed.sol b/contracts/products/hbXAUt/HBXautDataFeed.sol deleted file mode 100644 index 7e7af9df..00000000 --- a/contracts/products/hbXAUt/HBXautDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./HBXautMidasAccessControlRoles.sol"; - -/** - * @title HBXautDataFeed - * @notice DataFeed for hbXAUt product - * @author RedDuck Software - */ -contract HBXautDataFeed is DataFeed, HBXautMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return HB_XAUT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/hbXAUt/HBXautDepositVault.sol b/contracts/products/hbXAUt/HBXautDepositVault.sol deleted file mode 100644 index e6588bd2..00000000 --- a/contracts/products/hbXAUt/HBXautDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./HBXautMidasAccessControlRoles.sol"; - -/** - * @title HBXautDepositVault - * @notice Smart contract that handles hbXAUt minting - * @author RedDuck Software - */ -contract HBXautDepositVault is DepositVault, HBXautMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return HB_XAUT_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/hbXAUt/HBXautMidasAccessControlRoles.sol b/contracts/products/hbXAUt/HBXautMidasAccessControlRoles.sol deleted file mode 100644 index 6a70338f..00000000 --- a/contracts/products/hbXAUt/HBXautMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title HBXautMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for hbXAUt contracts - * @author RedDuck Software - */ -abstract contract HBXautMidasAccessControlRoles { - /** - * @notice actor that can manage HBXautDepositVault - */ - bytes32 public constant HB_XAUT_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("HB_XAUT_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage HBXautRedemptionVault - */ - bytes32 public constant HB_XAUT_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("HB_XAUT_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage HBXautCustomAggregatorFeed and HBXautDataFeed - */ - bytes32 public constant HB_XAUT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("HB_XAUT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/hbXAUt/HBXautRedemptionVaultWithSwapper.sol b/contracts/products/hbXAUt/HBXautRedemptionVaultWithSwapper.sol deleted file mode 100644 index 66c1c944..00000000 --- a/contracts/products/hbXAUt/HBXautRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./HBXautMidasAccessControlRoles.sol"; - -/** - * @title HBXautRedemptionVaultWithSwapper - * @notice Smart contract that handles hbXAUt redemptions - * @author RedDuck Software - */ -contract HBXautRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - HBXautMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return HB_XAUT_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/hbXAUt/hbXAUt.sol b/contracts/products/hbXAUt/hbXAUt.sol deleted file mode 100644 index 2cb7ee3b..00000000 --- a/contracts/products/hbXAUt/hbXAUt.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title hbXAUt - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract hbXAUt is mToken { - /** - * @notice actor that can mint hbXAUt - */ - bytes32 public constant HB_XAUT_MINT_OPERATOR_ROLE = - keccak256("HB_XAUT_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn hbXAUt - */ - bytes32 public constant HB_XAUT_BURN_OPERATOR_ROLE = - keccak256("HB_XAUT_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause hbXAUt - */ - bytes32 public constant HB_XAUT_PAUSE_OPERATOR_ROLE = - keccak256("HB_XAUT_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Hyperbeat XAUt", "hbXAUt"); - } - - /** - * @dev AC role, owner of which can mint hbXAUt token - */ - function _minterRole() internal pure override returns (bytes32) { - return HB_XAUT_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn hbXAUt token - */ - function _burnerRole() internal pure override returns (bytes32) { - return HB_XAUT_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause hbXAUt token - */ - function _pauserRole() internal pure override returns (bytes32) { - return HB_XAUT_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/hypeBTC/HypeBtcCustomAggregatorFeed.sol b/contracts/products/hypeBTC/HypeBtcCustomAggregatorFeed.sol deleted file mode 100644 index b689cc9d..00000000 --- a/contracts/products/hypeBTC/HypeBtcCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./HypeBtcMidasAccessControlRoles.sol"; - -/** - * @title HypeBtcCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for hypeBTC, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract HypeBtcCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - HypeBtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return HYPE_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/hypeBTC/HypeBtcDataFeed.sol b/contracts/products/hypeBTC/HypeBtcDataFeed.sol deleted file mode 100644 index 825d236a..00000000 --- a/contracts/products/hypeBTC/HypeBtcDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./HypeBtcMidasAccessControlRoles.sol"; - -/** - * @title HypeBtcDataFeed - * @notice DataFeed for hypeBTC product - * @author RedDuck Software - */ -contract HypeBtcDataFeed is DataFeed, HypeBtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return HYPE_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/hypeBTC/HypeBtcDepositVault.sol b/contracts/products/hypeBTC/HypeBtcDepositVault.sol deleted file mode 100644 index 4c378b7b..00000000 --- a/contracts/products/hypeBTC/HypeBtcDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./HypeBtcMidasAccessControlRoles.sol"; - -/** - * @title HypeBtcDepositVault - * @notice Smart contract that handles hypeBTC minting - * @author RedDuck Software - */ -contract HypeBtcDepositVault is DepositVault, HypeBtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return HYPE_BTC_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/hypeBTC/HypeBtcMidasAccessControlRoles.sol b/contracts/products/hypeBTC/HypeBtcMidasAccessControlRoles.sol deleted file mode 100644 index 848360ed..00000000 --- a/contracts/products/hypeBTC/HypeBtcMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title HypeBtcMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for hypeBTC contracts - * @author RedDuck Software - */ -abstract contract HypeBtcMidasAccessControlRoles { - /** - * @notice actor that can manage HypeBtcDepositVault - */ - bytes32 public constant HYPE_BTC_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("HYPE_BTC_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage HypeBtcRedemptionVault - */ - bytes32 public constant HYPE_BTC_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("HYPE_BTC_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage HypeBtcCustomAggregatorFeed and HypeBtcDataFeed - */ - bytes32 public constant HYPE_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("HYPE_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/hypeBTC/HypeBtcRedemptionVaultWithSwapper.sol b/contracts/products/hypeBTC/HypeBtcRedemptionVaultWithSwapper.sol deleted file mode 100644 index 642e78c8..00000000 --- a/contracts/products/hypeBTC/HypeBtcRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./HypeBtcMidasAccessControlRoles.sol"; - -/** - * @title HypeBtcRedemptionVaultWithSwapper - * @notice Smart contract that handles hypeBTC redemptions - * @author RedDuck Software - */ -contract HypeBtcRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - HypeBtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return HYPE_BTC_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/hypeBTC/hypeBTC.sol b/contracts/products/hypeBTC/hypeBTC.sol deleted file mode 100644 index cdb3a9d7..00000000 --- a/contracts/products/hypeBTC/hypeBTC.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title hypeBTC - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract hypeBTC is mToken { - /** - * @notice actor that can mint hypeBTC - */ - bytes32 public constant HYPE_BTC_MINT_OPERATOR_ROLE = - keccak256("HYPE_BTC_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn hypeBTC - */ - bytes32 public constant HYPE_BTC_BURN_OPERATOR_ROLE = - keccak256("HYPE_BTC_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause hypeBTC - */ - bytes32 public constant HYPE_BTC_PAUSE_OPERATOR_ROLE = - keccak256("HYPE_BTC_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("HyperBTC Vault", "hypeBTC"); - } - - /** - * @dev AC role, owner of which can mint hypeBTC token - */ - function _minterRole() internal pure override returns (bytes32) { - return HYPE_BTC_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn hypeBTC token - */ - function _burnerRole() internal pure override returns (bytes32) { - return HYPE_BTC_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause hypeBTC token - */ - function _pauserRole() internal pure override returns (bytes32) { - return HYPE_BTC_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/hypeETH/HypeEthCustomAggregatorFeed.sol b/contracts/products/hypeETH/HypeEthCustomAggregatorFeed.sol deleted file mode 100644 index 1e633afb..00000000 --- a/contracts/products/hypeETH/HypeEthCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./HypeEthMidasAccessControlRoles.sol"; - -/** - * @title HypeEthCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for hypeETH, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract HypeEthCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - HypeEthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return HYPE_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/hypeETH/HypeEthDataFeed.sol b/contracts/products/hypeETH/HypeEthDataFeed.sol deleted file mode 100644 index 875eb699..00000000 --- a/contracts/products/hypeETH/HypeEthDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./HypeEthMidasAccessControlRoles.sol"; - -/** - * @title HypeEthDataFeed - * @notice DataFeed for hypeETH product - * @author RedDuck Software - */ -contract HypeEthDataFeed is DataFeed, HypeEthMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return HYPE_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/hypeETH/HypeEthDepositVault.sol b/contracts/products/hypeETH/HypeEthDepositVault.sol deleted file mode 100644 index 63192c82..00000000 --- a/contracts/products/hypeETH/HypeEthDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./HypeEthMidasAccessControlRoles.sol"; - -/** - * @title HypeEthDepositVault - * @notice Smart contract that handles hypeETH minting - * @author RedDuck Software - */ -contract HypeEthDepositVault is DepositVault, HypeEthMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return HYPE_ETH_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/hypeETH/HypeEthMidasAccessControlRoles.sol b/contracts/products/hypeETH/HypeEthMidasAccessControlRoles.sol deleted file mode 100644 index 2af6d10c..00000000 --- a/contracts/products/hypeETH/HypeEthMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title HypeEthMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for hypeETH contracts - * @author RedDuck Software - */ -abstract contract HypeEthMidasAccessControlRoles { - /** - * @notice actor that can manage HypeEthDepositVault - */ - bytes32 public constant HYPE_ETH_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("HYPE_ETH_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage HypeEthRedemptionVault - */ - bytes32 public constant HYPE_ETH_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("HYPE_ETH_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage HypeEthCustomAggregatorFeed and HypeEthDataFeed - */ - bytes32 public constant HYPE_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("HYPE_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/hypeETH/HypeEthRedemptionVaultWithSwapper.sol b/contracts/products/hypeETH/HypeEthRedemptionVaultWithSwapper.sol deleted file mode 100644 index 9b711a6b..00000000 --- a/contracts/products/hypeETH/HypeEthRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./HypeEthMidasAccessControlRoles.sol"; - -/** - * @title HypeEthRedemptionVaultWithSwapper - * @notice Smart contract that handles hypeETH redemptions - * @author RedDuck Software - */ -contract HypeEthRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - HypeEthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return HYPE_ETH_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/hypeETH/hypeETH.sol b/contracts/products/hypeETH/hypeETH.sol deleted file mode 100644 index 21866ca9..00000000 --- a/contracts/products/hypeETH/hypeETH.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title hypeETH - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract hypeETH is mToken { - /** - * @notice actor that can mint hypeETH - */ - bytes32 public constant HYPE_ETH_MINT_OPERATOR_ROLE = - keccak256("HYPE_ETH_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn hypeETH - */ - bytes32 public constant HYPE_ETH_BURN_OPERATOR_ROLE = - keccak256("HYPE_ETH_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause hypeETH - */ - bytes32 public constant HYPE_ETH_PAUSE_OPERATOR_ROLE = - keccak256("HYPE_ETH_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("HyperETH Vault", "hypeETH"); - } - - /** - * @dev AC role, owner of which can mint hypeETH token - */ - function _minterRole() internal pure override returns (bytes32) { - return HYPE_ETH_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn hypeETH token - */ - function _burnerRole() internal pure override returns (bytes32) { - return HYPE_ETH_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause hypeETH token - */ - function _pauserRole() internal pure override returns (bytes32) { - return HYPE_ETH_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/hypeUSD/HypeUsdCustomAggregatorFeed.sol b/contracts/products/hypeUSD/HypeUsdCustomAggregatorFeed.sol deleted file mode 100644 index 1339cd2a..00000000 --- a/contracts/products/hypeUSD/HypeUsdCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./HypeUsdMidasAccessControlRoles.sol"; - -/** - * @title HypeUsdCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for hypeUSD, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract HypeUsdCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - HypeUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return HYPE_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/hypeUSD/HypeUsdDataFeed.sol b/contracts/products/hypeUSD/HypeUsdDataFeed.sol deleted file mode 100644 index 67767723..00000000 --- a/contracts/products/hypeUSD/HypeUsdDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./HypeUsdMidasAccessControlRoles.sol"; - -/** - * @title HypeUsdDataFeed - * @notice DataFeed for hypeUSD product - * @author RedDuck Software - */ -contract HypeUsdDataFeed is DataFeed, HypeUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return HYPE_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/hypeUSD/HypeUsdDepositVault.sol b/contracts/products/hypeUSD/HypeUsdDepositVault.sol deleted file mode 100644 index 44ed47b2..00000000 --- a/contracts/products/hypeUSD/HypeUsdDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./HypeUsdMidasAccessControlRoles.sol"; - -/** - * @title HypeUsdDepositVault - * @notice Smart contract that handles hypeUSD minting - * @author RedDuck Software - */ -contract HypeUsdDepositVault is DepositVault, HypeUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return HYPE_USD_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/hypeUSD/HypeUsdMidasAccessControlRoles.sol b/contracts/products/hypeUSD/HypeUsdMidasAccessControlRoles.sol deleted file mode 100644 index babcc9b6..00000000 --- a/contracts/products/hypeUSD/HypeUsdMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title HypeUsdMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for hypeUSD contracts - * @author RedDuck Software - */ -abstract contract HypeUsdMidasAccessControlRoles { - /** - * @notice actor that can manage HypeUsdDepositVault - */ - bytes32 public constant HYPE_USD_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("HYPE_USD_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage HypeUsdRedemptionVault - */ - bytes32 public constant HYPE_USD_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("HYPE_USD_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage HypeUsdCustomAggregatorFeed and HypeUsdDataFeed - */ - bytes32 public constant HYPE_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("HYPE_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/hypeUSD/HypeUsdRedemptionVaultWithSwapper.sol b/contracts/products/hypeUSD/HypeUsdRedemptionVaultWithSwapper.sol deleted file mode 100644 index 27a40fed..00000000 --- a/contracts/products/hypeUSD/HypeUsdRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./HypeUsdMidasAccessControlRoles.sol"; - -/** - * @title HypeUsdRedemptionVaultWithSwapper - * @notice Smart contract that handles hypeUSD redemptions - * @author RedDuck Software - */ -contract HypeUsdRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - HypeUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return HYPE_USD_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/hypeUSD/hypeUSD.sol b/contracts/products/hypeUSD/hypeUSD.sol deleted file mode 100644 index 70ef48d9..00000000 --- a/contracts/products/hypeUSD/hypeUSD.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title hypeUSD - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract hypeUSD is mToken { - /** - * @notice actor that can mint hypeUSD - */ - bytes32 public constant HYPE_USD_MINT_OPERATOR_ROLE = - keccak256("HYPE_USD_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn hypeUSD - */ - bytes32 public constant HYPE_USD_BURN_OPERATOR_ROLE = - keccak256("HYPE_USD_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause hypeUSD - */ - bytes32 public constant HYPE_USD_PAUSE_OPERATOR_ROLE = - keccak256("HYPE_USD_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("HyperUSD Vault", "hypeUSD"); - } - - /** - * @dev AC role, owner of which can mint hypeUSD token - */ - function _minterRole() internal pure override returns (bytes32) { - return HYPE_USD_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn hypeUSD token - */ - function _burnerRole() internal pure override returns (bytes32) { - return HYPE_USD_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause hypeUSD token - */ - function _pauserRole() internal pure override returns (bytes32) { - return HYPE_USD_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/kitBTC/KitBtcCustomAggregatorFeed.sol b/contracts/products/kitBTC/KitBtcCustomAggregatorFeed.sol deleted file mode 100644 index 9316309a..00000000 --- a/contracts/products/kitBTC/KitBtcCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./KitBtcMidasAccessControlRoles.sol"; - -/** - * @title KitBtcCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for kitBTC, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract KitBtcCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - KitBtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return KIT_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/kitBTC/KitBtcDataFeed.sol b/contracts/products/kitBTC/KitBtcDataFeed.sol deleted file mode 100644 index 7a2c396d..00000000 --- a/contracts/products/kitBTC/KitBtcDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./KitBtcMidasAccessControlRoles.sol"; - -/** - * @title KitBtcDataFeed - * @notice DataFeed for kitBTC product - * @author RedDuck Software - */ -contract KitBtcDataFeed is DataFeed, KitBtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return KIT_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/kitBTC/KitBtcDepositVault.sol b/contracts/products/kitBTC/KitBtcDepositVault.sol deleted file mode 100644 index 0d330030..00000000 --- a/contracts/products/kitBTC/KitBtcDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./KitBtcMidasAccessControlRoles.sol"; - -/** - * @title KitBtcDepositVault - * @notice Smart contract that handles kitBTC minting - * @author RedDuck Software - */ -contract KitBtcDepositVault is DepositVault, KitBtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return KIT_BTC_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/kitBTC/KitBtcMidasAccessControlRoles.sol b/contracts/products/kitBTC/KitBtcMidasAccessControlRoles.sol deleted file mode 100644 index 6230e6ac..00000000 --- a/contracts/products/kitBTC/KitBtcMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title KitBtcMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for kitBTC contracts - * @author RedDuck Software - */ -abstract contract KitBtcMidasAccessControlRoles { - /** - * @notice actor that can manage KitBtcDepositVault - */ - bytes32 public constant KIT_BTC_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("KIT_BTC_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage KitBtcRedemptionVault - */ - bytes32 public constant KIT_BTC_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("KIT_BTC_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage KitBtcCustomAggregatorFeed and KitBtcDataFeed - */ - bytes32 public constant KIT_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("KIT_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/kitBTC/KitBtcRedemptionVaultWithSwapper.sol b/contracts/products/kitBTC/KitBtcRedemptionVaultWithSwapper.sol deleted file mode 100644 index 2eaebcd3..00000000 --- a/contracts/products/kitBTC/KitBtcRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./KitBtcMidasAccessControlRoles.sol"; - -/** - * @title KitBtcRedemptionVaultWithSwapper - * @notice Smart contract that handles kitBTC redemptions - * @author RedDuck Software - */ -contract KitBtcRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - KitBtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return KIT_BTC_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/kitBTC/kitBTC.sol b/contracts/products/kitBTC/kitBTC.sol deleted file mode 100644 index 0e0db27a..00000000 --- a/contracts/products/kitBTC/kitBTC.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title kitBTC - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract kitBTC is mToken { - /** - * @notice actor that can mint kitBTC - */ - bytes32 public constant KIT_BTC_MINT_OPERATOR_ROLE = - keccak256("KIT_BTC_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn kitBTC - */ - bytes32 public constant KIT_BTC_BURN_OPERATOR_ROLE = - keccak256("KIT_BTC_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause kitBTC - */ - bytes32 public constant KIT_BTC_PAUSE_OPERATOR_ROLE = - keccak256("KIT_BTC_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Kitchen Pre-deposit $kitBTC", "$kitBTC"); - } - - /** - * @dev AC role, owner of which can mint kitBTC token - */ - function _minterRole() internal pure override returns (bytes32) { - return KIT_BTC_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn kitBTC token - */ - function _burnerRole() internal pure override returns (bytes32) { - return KIT_BTC_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause kitBTC token - */ - function _pauserRole() internal pure override returns (bytes32) { - return KIT_BTC_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/kitHYPE/KitHypeCustomAggregatorFeed.sol b/contracts/products/kitHYPE/KitHypeCustomAggregatorFeed.sol deleted file mode 100644 index a71e6d04..00000000 --- a/contracts/products/kitHYPE/KitHypeCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./KitHypeMidasAccessControlRoles.sol"; - -/** - * @title KitHypeCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for kitHYPE, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract KitHypeCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - KitHypeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return KIT_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/kitHYPE/KitHypeDataFeed.sol b/contracts/products/kitHYPE/KitHypeDataFeed.sol deleted file mode 100644 index 13eb7c5c..00000000 --- a/contracts/products/kitHYPE/KitHypeDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./KitHypeMidasAccessControlRoles.sol"; - -/** - * @title KitHypeDataFeed - * @notice DataFeed for kitHYPE product - * @author RedDuck Software - */ -contract KitHypeDataFeed is DataFeed, KitHypeMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return KIT_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/kitHYPE/KitHypeDepositVault.sol b/contracts/products/kitHYPE/KitHypeDepositVault.sol deleted file mode 100644 index d2faa1c4..00000000 --- a/contracts/products/kitHYPE/KitHypeDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./KitHypeMidasAccessControlRoles.sol"; - -/** - * @title KitHypeDepositVault - * @notice Smart contract that handles kitHYPE minting - * @author RedDuck Software - */ -contract KitHypeDepositVault is DepositVault, KitHypeMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return KIT_HYPE_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/kitHYPE/KitHypeMidasAccessControlRoles.sol b/contracts/products/kitHYPE/KitHypeMidasAccessControlRoles.sol deleted file mode 100644 index 629e026e..00000000 --- a/contracts/products/kitHYPE/KitHypeMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title KitHypeMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for kitHYPE contracts - * @author RedDuck Software - */ -abstract contract KitHypeMidasAccessControlRoles { - /** - * @notice actor that can manage KitHypeDepositVault - */ - bytes32 public constant KIT_HYPE_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("KIT_HYPE_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage KitHypeRedemptionVault - */ - bytes32 public constant KIT_HYPE_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("KIT_HYPE_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage KitHypeCustomAggregatorFeed and KitHypeDataFeed - */ - bytes32 public constant KIT_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("KIT_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/kitHYPE/KitHypeRedemptionVaultWithSwapper.sol b/contracts/products/kitHYPE/KitHypeRedemptionVaultWithSwapper.sol deleted file mode 100644 index fe528166..00000000 --- a/contracts/products/kitHYPE/KitHypeRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./KitHypeMidasAccessControlRoles.sol"; - -/** - * @title KitHypeRedemptionVaultWithSwapper - * @notice Smart contract that handles kitHYPE redemptions - * @author RedDuck Software - */ -contract KitHypeRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - KitHypeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return KIT_HYPE_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/kitHYPE/kitHYPE.sol b/contracts/products/kitHYPE/kitHYPE.sol deleted file mode 100644 index d5dd83b1..00000000 --- a/contracts/products/kitHYPE/kitHYPE.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title kitHYPE - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract kitHYPE is mToken { - /** - * @notice actor that can mint kitHYPE - */ - bytes32 public constant KIT_HYPE_MINT_OPERATOR_ROLE = - keccak256("KIT_HYPE_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn kitHYPE - */ - bytes32 public constant KIT_HYPE_BURN_OPERATOR_ROLE = - keccak256("KIT_HYPE_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause kitHYPE - */ - bytes32 public constant KIT_HYPE_PAUSE_OPERATOR_ROLE = - keccak256("KIT_HYPE_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Kitchen Pre-deposit $kitHYPE", "$kitHYPE"); - } - - /** - * @dev AC role, owner of which can mint kitHYPE token - */ - function _minterRole() internal pure override returns (bytes32) { - return KIT_HYPE_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn kitHYPE token - */ - function _burnerRole() internal pure override returns (bytes32) { - return KIT_HYPE_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause kitHYPE token - */ - function _pauserRole() internal pure override returns (bytes32) { - return KIT_HYPE_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/kitUSD/KitUsdCustomAggregatorFeed.sol b/contracts/products/kitUSD/KitUsdCustomAggregatorFeed.sol deleted file mode 100644 index b078a929..00000000 --- a/contracts/products/kitUSD/KitUsdCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./KitUsdMidasAccessControlRoles.sol"; - -/** - * @title KitUsdCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for kitUSD, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract KitUsdCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - KitUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return KIT_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/kitUSD/KitUsdDataFeed.sol b/contracts/products/kitUSD/KitUsdDataFeed.sol deleted file mode 100644 index d6e4874d..00000000 --- a/contracts/products/kitUSD/KitUsdDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./KitUsdMidasAccessControlRoles.sol"; - -/** - * @title KitUsdDataFeed - * @notice DataFeed for kitUSD product - * @author RedDuck Software - */ -contract KitUsdDataFeed is DataFeed, KitUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return KIT_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/kitUSD/KitUsdDepositVault.sol b/contracts/products/kitUSD/KitUsdDepositVault.sol deleted file mode 100644 index 6f0267b1..00000000 --- a/contracts/products/kitUSD/KitUsdDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./KitUsdMidasAccessControlRoles.sol"; - -/** - * @title KitUsdDepositVault - * @notice Smart contract that handles kitUSD minting - * @author RedDuck Software - */ -contract KitUsdDepositVault is DepositVault, KitUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return KIT_USD_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/kitUSD/KitUsdMidasAccessControlRoles.sol b/contracts/products/kitUSD/KitUsdMidasAccessControlRoles.sol deleted file mode 100644 index 202b19a7..00000000 --- a/contracts/products/kitUSD/KitUsdMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title KitUsdMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for kitUSD contracts - * @author RedDuck Software - */ -abstract contract KitUsdMidasAccessControlRoles { - /** - * @notice actor that can manage KitUsdDepositVault - */ - bytes32 public constant KIT_USD_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("KIT_USD_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage KitUsdRedemptionVault - */ - bytes32 public constant KIT_USD_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("KIT_USD_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage KitUsdCustomAggregatorFeed and KitUsdDataFeed - */ - bytes32 public constant KIT_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("KIT_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/kitUSD/KitUsdRedemptionVaultWithSwapper.sol b/contracts/products/kitUSD/KitUsdRedemptionVaultWithSwapper.sol deleted file mode 100644 index 99f48932..00000000 --- a/contracts/products/kitUSD/KitUsdRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./KitUsdMidasAccessControlRoles.sol"; - -/** - * @title KitUsdRedemptionVaultWithSwapper - * @notice Smart contract that handles kitUSD redemptions - * @author RedDuck Software - */ -contract KitUsdRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - KitUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return KIT_USD_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/kitUSD/kitUSD.sol b/contracts/products/kitUSD/kitUSD.sol deleted file mode 100644 index 07f42997..00000000 --- a/contracts/products/kitUSD/kitUSD.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title kitUSD - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract kitUSD is mToken { - /** - * @notice actor that can mint kitUSD - */ - bytes32 public constant KIT_USD_MINT_OPERATOR_ROLE = - keccak256("KIT_USD_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn kitUSD - */ - bytes32 public constant KIT_USD_BURN_OPERATOR_ROLE = - keccak256("KIT_USD_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause kitUSD - */ - bytes32 public constant KIT_USD_PAUSE_OPERATOR_ROLE = - keccak256("KIT_USD_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Kitchen Pre-deposit $kitUSD", "$kitUSD"); - } - - /** - * @dev AC role, owner of which can mint kitUSD token - */ - function _minterRole() internal pure override returns (bytes32) { - return KIT_USD_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn kitUSD token - */ - function _burnerRole() internal pure override returns (bytes32) { - return KIT_USD_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause kitUSD token - */ - function _pauserRole() internal pure override returns (bytes32) { - return KIT_USD_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/kmiUSD/KmiUsdCustomAggregatorFeed.sol b/contracts/products/kmiUSD/KmiUsdCustomAggregatorFeed.sol deleted file mode 100644 index 298bef64..00000000 --- a/contracts/products/kmiUSD/KmiUsdCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./KmiUsdMidasAccessControlRoles.sol"; - -/** - * @title KmiUsdCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for kmiUSD, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract KmiUsdCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - KmiUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return KMI_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/kmiUSD/KmiUsdDataFeed.sol b/contracts/products/kmiUSD/KmiUsdDataFeed.sol deleted file mode 100644 index 6065073b..00000000 --- a/contracts/products/kmiUSD/KmiUsdDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./KmiUsdMidasAccessControlRoles.sol"; - -/** - * @title KmiUsdDataFeed - * @notice DataFeed for kmiUSD product - * @author RedDuck Software - */ -contract KmiUsdDataFeed is DataFeed, KmiUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return KMI_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/kmiUSD/KmiUsdDepositVault.sol b/contracts/products/kmiUSD/KmiUsdDepositVault.sol deleted file mode 100644 index 25af1dc5..00000000 --- a/contracts/products/kmiUSD/KmiUsdDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./KmiUsdMidasAccessControlRoles.sol"; - -/** - * @title KmiUsdDepositVault - * @notice Smart contract that handles kmiUSD minting - * @author RedDuck Software - */ -contract KmiUsdDepositVault is DepositVault, KmiUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return KMI_USD_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/kmiUSD/KmiUsdMidasAccessControlRoles.sol b/contracts/products/kmiUSD/KmiUsdMidasAccessControlRoles.sol deleted file mode 100644 index 05c73d8e..00000000 --- a/contracts/products/kmiUSD/KmiUsdMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title KmiUsdMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for kmiUSD contracts - * @author RedDuck Software - */ -abstract contract KmiUsdMidasAccessControlRoles { - /** - * @notice actor that can manage KmiUsdDepositVault - */ - bytes32 public constant KMI_USD_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("KMI_USD_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage KmiUsdRedemptionVault - */ - bytes32 public constant KMI_USD_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("KMI_USD_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage KmiUsdCustomAggregatorFeed and KmiUsdDataFeed - */ - bytes32 public constant KMI_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("KMI_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/kmiUSD/KmiUsdRedemptionVaultWithSwapper.sol b/contracts/products/kmiUSD/KmiUsdRedemptionVaultWithSwapper.sol deleted file mode 100644 index 4219902f..00000000 --- a/contracts/products/kmiUSD/KmiUsdRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./KmiUsdMidasAccessControlRoles.sol"; - -/** - * @title KmiUsdRedemptionVaultWithSwapper - * @notice Smart contract that handles kmiUSD redemptions - * @author RedDuck Software - */ -contract KmiUsdRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - KmiUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return KMI_USD_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/kmiUSD/kmiUSD.sol b/contracts/products/kmiUSD/kmiUSD.sol deleted file mode 100644 index 9c4d5b31..00000000 --- a/contracts/products/kmiUSD/kmiUSD.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title kmiUSD - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract kmiUSD is mToken { - /** - * @notice actor that can mint kmiUSD - */ - bytes32 public constant KMI_USD_MINT_OPERATOR_ROLE = - keccak256("KMI_USD_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn kmiUSD - */ - bytes32 public constant KMI_USD_BURN_OPERATOR_ROLE = - keccak256("KMI_USD_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause kmiUSD - */ - bytes32 public constant KMI_USD_PAUSE_OPERATOR_ROLE = - keccak256("KMI_USD_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Katana miUSD", "kmiUSD"); - } - - /** - * @dev AC role, owner of which can mint kmiUSD token - */ - function _minterRole() internal pure override returns (bytes32) { - return KMI_USD_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn kmiUSD token - */ - function _burnerRole() internal pure override returns (bytes32) { - return KMI_USD_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause kmiUSD token - */ - function _pauserRole() internal pure override returns (bytes32) { - return KMI_USD_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/liquidHYPE/LiquidHypeCustomAggregatorFeed.sol b/contracts/products/liquidHYPE/LiquidHypeCustomAggregatorFeed.sol deleted file mode 100644 index dcb5882d..00000000 --- a/contracts/products/liquidHYPE/LiquidHypeCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./LiquidHypeMidasAccessControlRoles.sol"; - -/** - * @title LiquidHypeCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for liquidHYPE, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract LiquidHypeCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - LiquidHypeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return LIQUID_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/liquidHYPE/LiquidHypeDataFeed.sol b/contracts/products/liquidHYPE/LiquidHypeDataFeed.sol deleted file mode 100644 index 19ea3986..00000000 --- a/contracts/products/liquidHYPE/LiquidHypeDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./LiquidHypeMidasAccessControlRoles.sol"; - -/** - * @title LiquidHypeDataFeed - * @notice DataFeed for liquidHYPE product - * @author RedDuck Software - */ -contract LiquidHypeDataFeed is DataFeed, LiquidHypeMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return LIQUID_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/liquidHYPE/LiquidHypeDepositVault.sol b/contracts/products/liquidHYPE/LiquidHypeDepositVault.sol deleted file mode 100644 index 78400e70..00000000 --- a/contracts/products/liquidHYPE/LiquidHypeDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./LiquidHypeMidasAccessControlRoles.sol"; - -/** - * @title LiquidHypeDepositVault - * @notice Smart contract that handles liquidHYPE minting - * @author RedDuck Software - */ -contract LiquidHypeDepositVault is - DepositVault, - LiquidHypeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return LIQUID_HYPE_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/liquidHYPE/LiquidHypeMidasAccessControlRoles.sol b/contracts/products/liquidHYPE/LiquidHypeMidasAccessControlRoles.sol deleted file mode 100644 index da2f066a..00000000 --- a/contracts/products/liquidHYPE/LiquidHypeMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title LiquidHypeMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for liquidHYPE contracts - * @author RedDuck Software - */ -abstract contract LiquidHypeMidasAccessControlRoles { - /** - * @notice actor that can manage LiquidHypeDepositVault - */ - bytes32 public constant LIQUID_HYPE_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("LIQUID_HYPE_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage LiquidHypeRedemptionVault - */ - bytes32 public constant LIQUID_HYPE_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("LIQUID_HYPE_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage LiquidHypeCustomAggregatorFeed and LiquidHypeDataFeed - */ - bytes32 public constant LIQUID_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("LIQUID_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/liquidHYPE/LiquidHypeRedemptionVaultWithSwapper.sol b/contracts/products/liquidHYPE/LiquidHypeRedemptionVaultWithSwapper.sol deleted file mode 100644 index 65666573..00000000 --- a/contracts/products/liquidHYPE/LiquidHypeRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./LiquidHypeMidasAccessControlRoles.sol"; - -/** - * @title LiquidHypeRedemptionVaultWithSwapper - * @notice Smart contract that handles liquidHYPE redemptions - * @author RedDuck Software - */ -contract LiquidHypeRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - LiquidHypeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return LIQUID_HYPE_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/liquidHYPE/liquidHYPE.sol b/contracts/products/liquidHYPE/liquidHYPE.sol deleted file mode 100644 index 2e1007b3..00000000 --- a/contracts/products/liquidHYPE/liquidHYPE.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title liquidHYPE - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract liquidHYPE is mToken { - /** - * @notice actor that can mint liquidHYPE - */ - bytes32 public constant LIQUID_HYPE_MINT_OPERATOR_ROLE = - keccak256("LIQUID_HYPE_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn liquidHYPE - */ - bytes32 public constant LIQUID_HYPE_BURN_OPERATOR_ROLE = - keccak256("LIQUID_HYPE_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause liquidHYPE - */ - bytes32 public constant LIQUID_HYPE_PAUSE_OPERATOR_ROLE = - keccak256("LIQUID_HYPE_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Liquid HYPE Yield", "liquidHYPE"); - } - - /** - * @dev AC role, owner of which can mint liquidHYPE token - */ - function _minterRole() internal pure override returns (bytes32) { - return LIQUID_HYPE_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn liquidHYPE token - */ - function _burnerRole() internal pure override returns (bytes32) { - return LIQUID_HYPE_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause liquidHYPE token - */ - function _pauserRole() internal pure override returns (bytes32) { - return LIQUID_HYPE_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/liquidRESERVE/LiquidReserveCustomAggregatorFeed.sol b/contracts/products/liquidRESERVE/LiquidReserveCustomAggregatorFeed.sol deleted file mode 100644 index 5e316eaf..00000000 --- a/contracts/products/liquidRESERVE/LiquidReserveCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./LiquidReserveMidasAccessControlRoles.sol"; - -/** - * @title LiquidReserveCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for liquidRESERVE, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract LiquidReserveCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - LiquidReserveMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return LIQUID_RESERVE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/liquidRESERVE/LiquidReserveDataFeed.sol b/contracts/products/liquidRESERVE/LiquidReserveDataFeed.sol deleted file mode 100644 index e3b297c0..00000000 --- a/contracts/products/liquidRESERVE/LiquidReserveDataFeed.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./LiquidReserveMidasAccessControlRoles.sol"; - -/** - * @title LiquidReserveDataFeed - * @notice DataFeed for liquidRESERVE product - * @author RedDuck Software - */ -contract LiquidReserveDataFeed is - DataFeed, - LiquidReserveMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return LIQUID_RESERVE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/liquidRESERVE/LiquidReserveDepositVault.sol b/contracts/products/liquidRESERVE/LiquidReserveDepositVault.sol deleted file mode 100644 index b3538d93..00000000 --- a/contracts/products/liquidRESERVE/LiquidReserveDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./LiquidReserveMidasAccessControlRoles.sol"; - -/** - * @title LiquidReserveDepositVault - * @notice Smart contract that handles liquidRESERVE minting - * @author RedDuck Software - */ -contract LiquidReserveDepositVault is - DepositVault, - LiquidReserveMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return LIQUID_RESERVE_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/liquidRESERVE/LiquidReserveMidasAccessControlRoles.sol b/contracts/products/liquidRESERVE/LiquidReserveMidasAccessControlRoles.sol deleted file mode 100644 index 6e2245ed..00000000 --- a/contracts/products/liquidRESERVE/LiquidReserveMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title LiquidReserveMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for liquidRESERVE contracts - * @author RedDuck Software - */ -abstract contract LiquidReserveMidasAccessControlRoles { - /** - * @notice actor that can manage LiquidReserveDepositVault - */ - bytes32 public constant LIQUID_RESERVE_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("LIQUID_RESERVE_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage LiquidReserveRedemptionVault - */ - bytes32 public constant LIQUID_RESERVE_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("LIQUID_RESERVE_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage LiquidReserveCustomAggregatorFeed and LiquidReserveDataFeed - */ - bytes32 public constant LIQUID_RESERVE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("LIQUID_RESERVE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/liquidRESERVE/LiquidReserveRedemptionVaultWithSwapper.sol b/contracts/products/liquidRESERVE/LiquidReserveRedemptionVaultWithSwapper.sol deleted file mode 100644 index 84ac0446..00000000 --- a/contracts/products/liquidRESERVE/LiquidReserveRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./LiquidReserveMidasAccessControlRoles.sol"; - -/** - * @title LiquidReserveRedemptionVaultWithSwapper - * @notice Smart contract that handles liquidRESERVE redemptions - * @author RedDuck Software - */ -contract LiquidReserveRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - LiquidReserveMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return LIQUID_RESERVE_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/liquidRESERVE/liquidRESERVE.sol b/contracts/products/liquidRESERVE/liquidRESERVE.sol deleted file mode 100644 index 81fdb6b2..00000000 --- a/contracts/products/liquidRESERVE/liquidRESERVE.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title liquidRESERVE - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract liquidRESERVE is mToken { - /** - * @notice actor that can mint liquidRESERVE - */ - bytes32 public constant LIQUID_RESERVE_MINT_OPERATOR_ROLE = - keccak256("LIQUID_RESERVE_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn liquidRESERVE - */ - bytes32 public constant LIQUID_RESERVE_BURN_OPERATOR_ROLE = - keccak256("LIQUID_RESERVE_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause liquidRESERVE - */ - bytes32 public constant LIQUID_RESERVE_PAUSE_OPERATOR_ROLE = - keccak256("LIQUID_RESERVE_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Ether.Fi Liquid Reserve", "liquidRESERVE"); - } - - /** - * @dev AC role, owner of which can mint liquidRESERVE token - */ - function _minterRole() internal pure override returns (bytes32) { - return LIQUID_RESERVE_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn liquidRESERVE token - */ - function _burnerRole() internal pure override returns (bytes32) { - return LIQUID_RESERVE_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause liquidRESERVE token - */ - function _pauserRole() internal pure override returns (bytes32) { - return LIQUID_RESERVE_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/liquidRWA/LiquidRwaCustomAggregatorFeed.sol b/contracts/products/liquidRWA/LiquidRwaCustomAggregatorFeed.sol deleted file mode 100644 index db1d9bc3..00000000 --- a/contracts/products/liquidRWA/LiquidRwaCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./LiquidRwaMidasAccessControlRoles.sol"; - -/** - * @title LiquidRwaCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for liquidRWA, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract LiquidRwaCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - LiquidRwaMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return LIQUID_RWA_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/liquidRWA/LiquidRwaDataFeed.sol b/contracts/products/liquidRWA/LiquidRwaDataFeed.sol deleted file mode 100644 index 4a6213c2..00000000 --- a/contracts/products/liquidRWA/LiquidRwaDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./LiquidRwaMidasAccessControlRoles.sol"; - -/** - * @title LiquidRwaDataFeed - * @notice DataFeed for liquidRWA product - * @author RedDuck Software - */ -contract LiquidRwaDataFeed is DataFeed, LiquidRwaMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return LIQUID_RWA_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/liquidRWA/LiquidRwaDepositVault.sol b/contracts/products/liquidRWA/LiquidRwaDepositVault.sol deleted file mode 100644 index 2e2658fb..00000000 --- a/contracts/products/liquidRWA/LiquidRwaDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./LiquidRwaMidasAccessControlRoles.sol"; - -/** - * @title LiquidRwaDepositVault - * @notice Smart contract that handles liquidRWA minting - * @author RedDuck Software - */ -contract LiquidRwaDepositVault is - DepositVault, - LiquidRwaMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return LIQUID_RWA_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/liquidRWA/LiquidRwaMidasAccessControlRoles.sol b/contracts/products/liquidRWA/LiquidRwaMidasAccessControlRoles.sol deleted file mode 100644 index 2d0c4d71..00000000 --- a/contracts/products/liquidRWA/LiquidRwaMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title LiquidRwaMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for liquidRWA contracts - * @author RedDuck Software - */ -abstract contract LiquidRwaMidasAccessControlRoles { - /** - * @notice actor that can manage LiquidRwaDepositVault - */ - bytes32 public constant LIQUID_RWA_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("LIQUID_RWA_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage LiquidRwaRedemptionVault - */ - bytes32 public constant LIQUID_RWA_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("LIQUID_RWA_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage LiquidRwaCustomAggregatorFeed and LiquidRwaDataFeed - */ - bytes32 public constant LIQUID_RWA_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("LIQUID_RWA_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/liquidRWA/LiquidRwaRedemptionVaultWithSwapper.sol b/contracts/products/liquidRWA/LiquidRwaRedemptionVaultWithSwapper.sol deleted file mode 100644 index f21416ed..00000000 --- a/contracts/products/liquidRWA/LiquidRwaRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./LiquidRwaMidasAccessControlRoles.sol"; - -/** - * @title LiquidRwaRedemptionVaultWithSwapper - * @notice Smart contract that handles liquidRWA redemptions - * @author RedDuck Software - */ -contract LiquidRwaRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - LiquidRwaMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return LIQUID_RWA_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/liquidRWA/liquidRWA.sol b/contracts/products/liquidRWA/liquidRWA.sol deleted file mode 100644 index 8446ce84..00000000 --- a/contracts/products/liquidRWA/liquidRWA.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title liquidRWA - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract liquidRWA is mToken { - /** - * @notice actor that can mint liquidRWA - */ - bytes32 public constant LIQUID_RWA_MINT_OPERATOR_ROLE = - keccak256("LIQUID_RWA_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn liquidRWA - */ - bytes32 public constant LIQUID_RWA_BURN_OPERATOR_ROLE = - keccak256("LIQUID_RWA_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause liquidRWA - */ - bytes32 public constant LIQUID_RWA_PAUSE_OPERATOR_ROLE = - keccak256("LIQUID_RWA_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Ether.fi Liquid RWA", "liquidRWA"); - } - - /** - * @dev AC role, owner of which can mint liquidRWA token - */ - function _minterRole() internal pure override returns (bytes32) { - return LIQUID_RWA_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn liquidRWA token - */ - function _burnerRole() internal pure override returns (bytes32) { - return LIQUID_RWA_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause liquidRWA token - */ - function _pauserRole() internal pure override returns (bytes32) { - return LIQUID_RWA_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/lstHYPE/LstHypeCustomAggregatorFeed.sol b/contracts/products/lstHYPE/LstHypeCustomAggregatorFeed.sol deleted file mode 100644 index c95fc906..00000000 --- a/contracts/products/lstHYPE/LstHypeCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./LstHypeMidasAccessControlRoles.sol"; - -/** - * @title LstHypeCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for lstHYPE, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract LstHypeCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - LstHypeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return LST_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/lstHYPE/LstHypeDataFeed.sol b/contracts/products/lstHYPE/LstHypeDataFeed.sol deleted file mode 100644 index ae42a67b..00000000 --- a/contracts/products/lstHYPE/LstHypeDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./LstHypeMidasAccessControlRoles.sol"; - -/** - * @title LstHypeDataFeed - * @notice DataFeed for lstHYPE product - * @author RedDuck Software - */ -contract LstHypeDataFeed is DataFeed, LstHypeMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return LST_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/lstHYPE/LstHypeDepositVault.sol b/contracts/products/lstHYPE/LstHypeDepositVault.sol deleted file mode 100644 index b83b6a6f..00000000 --- a/contracts/products/lstHYPE/LstHypeDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./LstHypeMidasAccessControlRoles.sol"; - -/** - * @title LstHypeDepositVault - * @notice Smart contract that handles LstHype minting - * @author RedDuck Software - */ -contract LstHypeDepositVault is DepositVault, LstHypeMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return LST_HYPE_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/lstHYPE/LstHypeMidasAccessControlRoles.sol b/contracts/products/lstHYPE/LstHypeMidasAccessControlRoles.sol deleted file mode 100644 index 628dc227..00000000 --- a/contracts/products/lstHYPE/LstHypeMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title LstHypeMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for lstHYPE contracts - * @author RedDuck Software - */ -abstract contract LstHypeMidasAccessControlRoles { - /** - * @notice actor that can manage LstHypeDepositVault - */ - bytes32 public constant LST_HYPE_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("LST_HYPE_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage LstHypeRedemptionVault - */ - bytes32 public constant LST_HYPE_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("LST_HYPE_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage LstHypeCustomAggregatorFeed and LstHypeDataFeed - */ - bytes32 public constant LST_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("LST_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/lstHYPE/LstHypeRedemptionVaultWithSwapper.sol b/contracts/products/lstHYPE/LstHypeRedemptionVaultWithSwapper.sol deleted file mode 100644 index d13e4c10..00000000 --- a/contracts/products/lstHYPE/LstHypeRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./LstHypeMidasAccessControlRoles.sol"; - -/** - * @title LstHypeRedemptionVaultWithSwapper - * @notice Smart contract that handles lstHYPE redemptions - * @author RedDuck Software - */ -contract LstHypeRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - LstHypeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return LST_HYPE_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/lstHYPE/lstHYPE.sol b/contracts/products/lstHYPE/lstHYPE.sol deleted file mode 100644 index b43c7fc0..00000000 --- a/contracts/products/lstHYPE/lstHYPE.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title lstHYPE - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract lstHYPE is mToken { - /** - * @notice actor that can mint lstHYPE - */ - bytes32 public constant LST_HYPE_MINT_OPERATOR_ROLE = - keccak256("LST_HYPE_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn lstHYPE - */ - bytes32 public constant LST_HYPE_BURN_OPERATOR_ROLE = - keccak256("LST_HYPE_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause lstHYPE - */ - bytes32 public constant LST_HYPE_PAUSE_OPERATOR_ROLE = - keccak256("LST_HYPE_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Hyperbeat LST Vault", "lstHYPE"); - } - - /** - * @dev AC role, owner of which can mint lstHYPE token - */ - function _minterRole() internal pure override returns (bytes32) { - return LST_HYPE_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn lstHYPE token - */ - function _burnerRole() internal pure override returns (bytes32) { - return LST_HYPE_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause lstHYPE token - */ - function _pauserRole() internal pure override returns (bytes32) { - return LST_HYPE_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mAPOLLO/MApolloCustomAggregatorFeed.sol b/contracts/products/mAPOLLO/MApolloCustomAggregatorFeed.sol deleted file mode 100644 index d26a3852..00000000 --- a/contracts/products/mAPOLLO/MApolloCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MApolloMidasAccessControlRoles.sol"; - -/** - * @title MApolloCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mAPOLLO, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MApolloCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MApolloMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_APOLLO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mAPOLLO/MApolloDataFeed.sol b/contracts/products/mAPOLLO/MApolloDataFeed.sol deleted file mode 100644 index f4f8363d..00000000 --- a/contracts/products/mAPOLLO/MApolloDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MApolloMidasAccessControlRoles.sol"; - -/** - * @title MApolloDataFeed - * @notice DataFeed for mAPOLLO product - * @author RedDuck Software - */ -contract MApolloDataFeed is DataFeed, MApolloMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_APOLLO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mAPOLLO/MApolloDepositVault.sol b/contracts/products/mAPOLLO/MApolloDepositVault.sol deleted file mode 100644 index 48710f7a..00000000 --- a/contracts/products/mAPOLLO/MApolloDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MApolloMidasAccessControlRoles.sol"; - -/** - * @title MApolloDepositVault - * @notice Smart contract that handles mAPOLLO minting - * @author RedDuck Software - */ -contract MApolloDepositVault is DepositVault, MApolloMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_APOLLO_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mAPOLLO/MApolloMidasAccessControlRoles.sol b/contracts/products/mAPOLLO/MApolloMidasAccessControlRoles.sol deleted file mode 100644 index 346a661c..00000000 --- a/contracts/products/mAPOLLO/MApolloMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MApolloMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mAPOLLO contracts - * @author RedDuck Software - */ -abstract contract MApolloMidasAccessControlRoles { - /** - * @notice actor that can manage MApolloDepositVault - */ - bytes32 public constant M_APOLLO_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_APOLLO_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MApolloRedemptionVault - */ - bytes32 public constant M_APOLLO_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_APOLLO_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MApolloCustomAggregatorFeed and MApolloDataFeed - */ - bytes32 public constant M_APOLLO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_APOLLO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mAPOLLO/MApolloRedemptionVaultWithSwapper.sol b/contracts/products/mAPOLLO/MApolloRedemptionVaultWithSwapper.sol deleted file mode 100644 index 02f51d57..00000000 --- a/contracts/products/mAPOLLO/MApolloRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MApolloMidasAccessControlRoles.sol"; - -/** - * @title MApolloRedemptionVaultWithSwapper - * @notice Smart contract that handles mAPOLLO redemptions - * @author RedDuck Software - */ -contract MApolloRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MApolloMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_APOLLO_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mAPOLLO/mAPOLLO.sol b/contracts/products/mAPOLLO/mAPOLLO.sol deleted file mode 100644 index 266c3251..00000000 --- a/contracts/products/mAPOLLO/mAPOLLO.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title mAPOLLO - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mAPOLLO is mToken { - /** - * @notice actor that can mint mAPOLLO - */ - bytes32 public constant M_APOLLO_MINT_OPERATOR_ROLE = - keccak256("M_APOLLO_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mAPOLLO - */ - bytes32 public constant M_APOLLO_BURN_OPERATOR_ROLE = - keccak256("M_APOLLO_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mAPOLLO - */ - bytes32 public constant M_APOLLO_PAUSE_OPERATOR_ROLE = - keccak256("M_APOLLO_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Apollo Crypto", "mAPOLLO"); - } - - /** - * @dev AC role, owner of which can mint mAPOLLO token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_APOLLO_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mAPOLLO token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_APOLLO_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mAPOLLO token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_APOLLO_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mBASIS/MBasisCustomAggregatorFeed.sol b/contracts/products/mBASIS/MBasisCustomAggregatorFeed.sol deleted file mode 100644 index c46ad9db..00000000 --- a/contracts/products/mBASIS/MBasisCustomAggregatorFeed.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MBasisMidasAccessControlRoles.sol"; - -/** - * @title MBasisCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mBASIS, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MBasisCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MBasisMidasAccessControlRoles -{ - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_BASIS_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mBASIS/MBasisDataFeed.sol b/contracts/products/mBASIS/MBasisDataFeed.sol deleted file mode 100644 index bdeeea50..00000000 --- a/contracts/products/mBASIS/MBasisDataFeed.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MBasisMidasAccessControlRoles.sol"; - -/** - * @title MBasisDataFeed - * @notice DataFeed for mBASIS product - * @author RedDuck Software - */ -contract MBasisDataFeed is DataFeed, MBasisMidasAccessControlRoles { - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_BASIS_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mBASIS/MBasisDepositVault.sol b/contracts/products/mBASIS/MBasisDepositVault.sol deleted file mode 100644 index e752a7f8..00000000 --- a/contracts/products/mBASIS/MBasisDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MBasisMidasAccessControlRoles.sol"; - -/** - * @title MBasisDepositVault - * @notice Smart contract that handles mBASIS minting - * @author RedDuck Software - */ -contract MBasisDepositVault is DepositVault, MBasisMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_BASIS_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mBASIS/MBasisMidasAccessControlRoles.sol b/contracts/products/mBASIS/MBasisMidasAccessControlRoles.sol deleted file mode 100644 index c95dfe89..00000000 --- a/contracts/products/mBASIS/MBasisMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MBasisMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mBASIS contracts - * @author RedDuck Software - */ -abstract contract MBasisMidasAccessControlRoles { - /** - * @notice actor that can manage MBasisDepositVault - */ - bytes32 public constant M_BASIS_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_BASIS_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MBasisRedemptionVault - */ - bytes32 public constant M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MBasisCustomAggregatorFeed and MBasisDataFeed - */ - bytes32 public constant M_BASIS_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_BASIS_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mBASIS/MBasisRedemptionVault.sol b/contracts/products/mBASIS/MBasisRedemptionVault.sol deleted file mode 100644 index 8537f0e9..00000000 --- a/contracts/products/mBASIS/MBasisRedemptionVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVault.sol"; -import "./MBasisMidasAccessControlRoles.sol"; - -/** - * @title MBasisRedemptionVault - * @notice Smart contract that handles mBASIS minting - * @author RedDuck Software - */ -contract MBasisRedemptionVault is - RedemptionVault, - MBasisMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mBASIS/MBasisRedemptionVaultWithBUIDL.sol b/contracts/products/mBASIS/MBasisRedemptionVaultWithBUIDL.sol deleted file mode 100644 index f24a266c..00000000 --- a/contracts/products/mBASIS/MBasisRedemptionVaultWithBUIDL.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithBUIDL.sol"; -import "./MBasisMidasAccessControlRoles.sol"; - -/** - * @title MBasisRedemptionVaultWithBUIDL - * @notice Smart contract that handles mBASIS minting - * @author RedDuck Software - */ -contract MBasisRedemptionVaultWithBUIDL is - RedemptionVaultWIthBUIDL, - MBasisMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mBASIS/MBasisRedemptionVaultWithSwapper.sol b/contracts/products/mBASIS/MBasisRedemptionVaultWithSwapper.sol deleted file mode 100644 index 0642d2ce..00000000 --- a/contracts/products/mBASIS/MBasisRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MBasisMidasAccessControlRoles.sol"; - -/** - * @title MBasisRedemptionVault - * @notice Smart contract that handles mBASIS redemptions - * @author RedDuck Software - */ -contract MBasisRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MBasisMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mBASIS/mBASIS.sol b/contracts/products/mBASIS/mBASIS.sol deleted file mode 100644 index 6211c8e5..00000000 --- a/contracts/products/mBASIS/mBASIS.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title mBASIS - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mBASIS is mToken { - /** - * @notice actor that can mint mBASIS - */ - bytes32 public constant M_BASIS_MINT_OPERATOR_ROLE = - keccak256("M_BASIS_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mBASIS - */ - bytes32 public constant M_BASIS_BURN_OPERATOR_ROLE = - keccak256("M_BASIS_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mBASIS - */ - bytes32 public constant M_BASIS_PAUSE_OPERATOR_ROLE = - keccak256("M_BASIS_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Basis Trading Token", "mBASIS"); - } - - /** - * @dev AC role, owner of which can mint mBASIS token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_BASIS_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mBASIS token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_BASIS_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mBASIS token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_BASIS_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mBTC/MBtcCustomAggregatorFeed.sol b/contracts/products/mBTC/MBtcCustomAggregatorFeed.sol deleted file mode 100644 index a42ca317..00000000 --- a/contracts/products/mBTC/MBtcCustomAggregatorFeed.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MBtcMidasAccessControlRoles.sol"; - -/** - * @title MBtcCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mBTC, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MBtcCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MBtcMidasAccessControlRoles -{ - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mBTC/MBtcDataFeed.sol b/contracts/products/mBTC/MBtcDataFeed.sol deleted file mode 100644 index ce7fbdef..00000000 --- a/contracts/products/mBTC/MBtcDataFeed.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MBtcMidasAccessControlRoles.sol"; - -/** - * @title MBtcDataFeed - * @notice DataFeed for mBTC product - * @author RedDuck Software - */ -contract MBtcDataFeed is DataFeed, MBtcMidasAccessControlRoles { - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mBTC/MBtcDepositVault.sol b/contracts/products/mBTC/MBtcDepositVault.sol deleted file mode 100644 index 858a5948..00000000 --- a/contracts/products/mBTC/MBtcDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MBtcMidasAccessControlRoles.sol"; - -/** - * @title MBtcDepositVault - * @notice Smart contract that handles mBTC minting - * @author RedDuck Software - */ -contract MBtcDepositVault is DepositVault, MBtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_BTC_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mBTC/MBtcMidasAccessControlRoles.sol b/contracts/products/mBTC/MBtcMidasAccessControlRoles.sol deleted file mode 100644 index 4f809aff..00000000 --- a/contracts/products/mBTC/MBtcMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MBtcMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mBTC contracts - * @author RedDuck Software - */ -abstract contract MBtcMidasAccessControlRoles { - /** - * @notice actor that can manage MBtcDepositVault - */ - bytes32 public constant M_BTC_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_BTC_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MBtcRedemptionVault - */ - bytes32 public constant M_BTC_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_BTC_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MBtcCustomAggregatorFeed and MBtcDataFeed - */ - bytes32 public constant M_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mBTC/MBtcRedemptionVault.sol b/contracts/products/mBTC/MBtcRedemptionVault.sol deleted file mode 100644 index 33af194b..00000000 --- a/contracts/products/mBTC/MBtcRedemptionVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVault.sol"; -import "./MBtcMidasAccessControlRoles.sol"; - -/** - * @title MBtcRedemptionVault - * @notice Smart contract that handles mBTC redemption - * @author RedDuck Software - */ -contract MBtcRedemptionVault is RedemptionVault, MBtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_BTC_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mBTC/mBTC.sol b/contracts/products/mBTC/mBTC.sol deleted file mode 100644 index 3eed97b9..00000000 --- a/contracts/products/mBTC/mBTC.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title mBTC - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mBTC is mToken { - /** - * @notice actor that can mint mBTC - */ - bytes32 public constant M_BTC_MINT_OPERATOR_ROLE = - keccak256("M_BTC_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mBTC - */ - bytes32 public constant M_BTC_BURN_OPERATOR_ROLE = - keccak256("M_BTC_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mBTC - */ - bytes32 public constant M_BTC_PAUSE_OPERATOR_ROLE = - keccak256("M_BTC_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas BTC Yield Token", "mBTC"); - } - - /** - * @dev AC role, owner of which can mint mBTC token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_BTC_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mBTC token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_BTC_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mBTC token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_BTC_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mBTC/tac/TACmBTC.sol b/contracts/products/mBTC/tac/TACmBTC.sol deleted file mode 100644 index 94437d9d..00000000 --- a/contracts/products/mBTC/tac/TACmBTC.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../../mToken.sol"; - -/** - * @title TACmBTC - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract TACmBTC is mToken { - /** - * @notice actor that can mint TACmBTC - */ - bytes32 public constant TAC_M_BTC_MINT_OPERATOR_ROLE = - keccak256("TAC_M_BTC_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn TACmBTC - */ - bytes32 public constant TAC_M_BTC_BURN_OPERATOR_ROLE = - keccak256("TAC_M_BTC_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause TACmBTC - */ - bytes32 public constant TAC_M_BTC_PAUSE_OPERATOR_ROLE = - keccak256("TAC_M_BTC_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas TACmBTC Token", "TACmBTC"); - } - - /** - * @dev AC role, owner of which can mint TACmBTC token - */ - function _minterRole() internal pure override returns (bytes32) { - return TAC_M_BTC_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn TACmBTC token - */ - function _burnerRole() internal pure override returns (bytes32) { - return TAC_M_BTC_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause TACmBTC token - */ - function _pauserRole() internal pure override returns (bytes32) { - return TAC_M_BTC_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mBTC/tac/TACmBtcDepositVault.sol b/contracts/products/mBTC/tac/TACmBtcDepositVault.sol deleted file mode 100644 index 835975cb..00000000 --- a/contracts/products/mBTC/tac/TACmBtcDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../../DepositVault.sol"; -import "./TACmBtcMidasAccessControlRoles.sol"; - -/** - * @title TACmBtcDepositVault - * @notice Smart contract that handles TACmBTC minting - * @author RedDuck Software - */ -contract TACmBtcDepositVault is DepositVault, TACmBtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return TAC_M_BTC_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mBTC/tac/TACmBtcMidasAccessControlRoles.sol b/contracts/products/mBTC/tac/TACmBtcMidasAccessControlRoles.sol deleted file mode 100644 index 1e7ac3bd..00000000 --- a/contracts/products/mBTC/tac/TACmBtcMidasAccessControlRoles.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title TACmBtcMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for TACmBTC contracts - * @author RedDuck Software - */ -abstract contract TACmBtcMidasAccessControlRoles { - /** - * @notice actor that can manage TACmBtcDepositVault - */ - bytes32 public constant TAC_M_BTC_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("TAC_M_BTC_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage TACmBtcRedemptionVault - */ - bytes32 public constant TAC_M_BTC_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("TAC_M_BTC_REDEMPTION_VAULT_ADMIN_ROLE"); -} diff --git a/contracts/products/mBTC/tac/TACmBtcRedemptionVault.sol b/contracts/products/mBTC/tac/TACmBtcRedemptionVault.sol deleted file mode 100644 index 047f5dfa..00000000 --- a/contracts/products/mBTC/tac/TACmBtcRedemptionVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../../RedemptionVault.sol"; -import "./TACmBtcMidasAccessControlRoles.sol"; - -/** - * @title TACmBtcRedemptionVault - * @notice Smart contract that handles TACmBTC redemption - * @author RedDuck Software - */ -contract TACmBtcRedemptionVault is - RedemptionVault, - TACmBtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return TAC_M_BTC_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEDGE/MEdgeCustomAggregatorFeed.sol b/contracts/products/mEDGE/MEdgeCustomAggregatorFeed.sol deleted file mode 100644 index beb6be38..00000000 --- a/contracts/products/mEDGE/MEdgeCustomAggregatorFeed.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MEdgeMidasAccessControlRoles.sol"; - -/** - * @title MEdgeCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mEDGE, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MEdgeCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MEdgeMidasAccessControlRoles -{ - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_EDGE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEDGE/MEdgeDataFeed.sol b/contracts/products/mEDGE/MEdgeDataFeed.sol deleted file mode 100644 index 8e25ad90..00000000 --- a/contracts/products/mEDGE/MEdgeDataFeed.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MEdgeMidasAccessControlRoles.sol"; - -/** - * @title MEdgeDataFeed - * @notice DataFeed for mEDGE product - * @author RedDuck Software - */ -contract MEdgeDataFeed is DataFeed, MEdgeMidasAccessControlRoles { - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_EDGE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEDGE/MEdgeDepositVault.sol b/contracts/products/mEDGE/MEdgeDepositVault.sol deleted file mode 100644 index 99a0dd04..00000000 --- a/contracts/products/mEDGE/MEdgeDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MEdgeMidasAccessControlRoles.sol"; - -/** - * @title MEdgeDepositVault - * @notice Smart contract that handles mEDGE minting - * @author RedDuck Software - */ -contract MEdgeDepositVault is DepositVault, MEdgeMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_EDGE_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEDGE/MEdgeMidasAccessControlRoles.sol b/contracts/products/mEDGE/MEdgeMidasAccessControlRoles.sol deleted file mode 100644 index f9c51220..00000000 --- a/contracts/products/mEDGE/MEdgeMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MEdgeMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mEDGE contracts - * @author RedDuck Software - */ -abstract contract MEdgeMidasAccessControlRoles { - /** - * @notice actor that can manage MEdgeDepositVault - */ - bytes32 public constant M_EDGE_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_EDGE_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MEdgeRedemptionVault - */ - bytes32 public constant M_EDGE_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_EDGE_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MEdgeCustomAggregatorFeed and MEdgeDataFeed - */ - bytes32 public constant M_EDGE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_EDGE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mEDGE/MEdgeRedemptionVaultWithSwapper.sol b/contracts/products/mEDGE/MEdgeRedemptionVaultWithSwapper.sol deleted file mode 100644 index c3c2c66e..00000000 --- a/contracts/products/mEDGE/MEdgeRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MEdgeMidasAccessControlRoles.sol"; - -/** - * @title MEdgeRedemptionVaultWithSwapper - * @notice Smart contract that handles mEDGE redemptions - * @author RedDuck Software - */ -contract MEdgeRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MEdgeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_EDGE_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEDGE/mEDGE.sol b/contracts/products/mEDGE/mEDGE.sol deleted file mode 100644 index cab4a5b1..00000000 --- a/contracts/products/mEDGE/mEDGE.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title mEDGE - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mEDGE is mToken { - /** - * @notice actor that can mint mEDGE - */ - bytes32 public constant M_EDGE_MINT_OPERATOR_ROLE = - keccak256("M_EDGE_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mEDGE - */ - bytes32 public constant M_EDGE_BURN_OPERATOR_ROLE = - keccak256("M_EDGE_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mEDGE - */ - bytes32 public constant M_EDGE_PAUSE_OPERATOR_ROLE = - keccak256("M_EDGE_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas mEDGE", "mEDGE"); - } - - /** - * @dev AC role, owner of which can mint mEDGE token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_EDGE_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mEDGE token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_EDGE_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mEDGE token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_EDGE_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mEDGE/tac/TACmEDGE.sol b/contracts/products/mEDGE/tac/TACmEDGE.sol deleted file mode 100644 index 38c9cd3b..00000000 --- a/contracts/products/mEDGE/tac/TACmEDGE.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../../mToken.sol"; - -/** - * @title TACmEDGE - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract TACmEDGE is mToken { - /** - * @notice actor that can mint TACmEDGE - */ - bytes32 public constant TAC_M_EDGE_MINT_OPERATOR_ROLE = - keccak256("TAC_M_EDGE_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn TACmEDGE - */ - bytes32 public constant TAC_M_EDGE_BURN_OPERATOR_ROLE = - keccak256("TAC_M_EDGE_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause TACmEDGE - */ - bytes32 public constant TAC_M_EDGE_PAUSE_OPERATOR_ROLE = - keccak256("TAC_M_EDGE_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas TACmEDGE Token", "TACmEDGE"); - } - - /** - * @dev AC role, owner of which can mint TACmEDGE token - */ - function _minterRole() internal pure override returns (bytes32) { - return TAC_M_EDGE_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn TACmEDGE token - */ - function _burnerRole() internal pure override returns (bytes32) { - return TAC_M_EDGE_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause TACmEDGE token - */ - function _pauserRole() internal pure override returns (bytes32) { - return TAC_M_EDGE_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mEDGE/tac/TACmEdgeDepositVault.sol b/contracts/products/mEDGE/tac/TACmEdgeDepositVault.sol deleted file mode 100644 index 2ecbee10..00000000 --- a/contracts/products/mEDGE/tac/TACmEdgeDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../../DepositVault.sol"; -import "./TACmEdgeMidasAccessControlRoles.sol"; - -/** - * @title TACmEdgeDepositVault - * @notice Smart contract that handles TACmEdge minting - * @author RedDuck Software - */ -contract TACmEdgeDepositVault is DepositVault, TACmEdgeMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return TAC_M_EDGE_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEDGE/tac/TACmEdgeMidasAccessControlRoles.sol b/contracts/products/mEDGE/tac/TACmEdgeMidasAccessControlRoles.sol deleted file mode 100644 index c394a542..00000000 --- a/contracts/products/mEDGE/tac/TACmEdgeMidasAccessControlRoles.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title TACmEdgeMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for TACmEdge contracts - * @author RedDuck Software - */ -abstract contract TACmEdgeMidasAccessControlRoles { - /** - * @notice actor that can manage TACmEdgeDepositVault - */ - bytes32 public constant TAC_M_EDGE_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("TAC_M_EDGE_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage TACmEdgeRedemptionVault - */ - bytes32 public constant TAC_M_EDGE_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("TAC_M_EDGE_REDEMPTION_VAULT_ADMIN_ROLE"); -} diff --git a/contracts/products/mEDGE/tac/TACmEdgeRedemptionVault.sol b/contracts/products/mEDGE/tac/TACmEdgeRedemptionVault.sol deleted file mode 100644 index 38d838b2..00000000 --- a/contracts/products/mEDGE/tac/TACmEdgeRedemptionVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../../RedemptionVault.sol"; -import "./TACmEdgeMidasAccessControlRoles.sol"; - -/** - * @title TACmEdgeRedemptionVault - * @notice Smart contract that handles TACmEDGE redemption - * @author RedDuck Software - */ -contract TACmEdgeRedemptionVault is - RedemptionVault, - TACmEdgeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return TAC_M_EDGE_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEVETH/MEvEthCustomAggregatorFeed.sol b/contracts/products/mEVETH/MEvEthCustomAggregatorFeed.sol deleted file mode 100644 index 62cc268e..00000000 --- a/contracts/products/mEVETH/MEvEthCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MEvEthMidasAccessControlRoles.sol"; - -/** - * @title MEvEthCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mEVETH, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MEvEthCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MEvEthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_EV_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEVETH/MEvEthDataFeed.sol b/contracts/products/mEVETH/MEvEthDataFeed.sol deleted file mode 100644 index 8e196f2f..00000000 --- a/contracts/products/mEVETH/MEvEthDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MEvEthMidasAccessControlRoles.sol"; - -/** - * @title MEvEthDataFeed - * @notice DataFeed for mEVETH product - * @author RedDuck Software - */ -contract MEvEthDataFeed is DataFeed, MEvEthMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_EV_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEVETH/MEvEthDepositVault.sol b/contracts/products/mEVETH/MEvEthDepositVault.sol deleted file mode 100644 index bf0a8ae0..00000000 --- a/contracts/products/mEVETH/MEvEthDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MEvEthMidasAccessControlRoles.sol"; - -/** - * @title MEvEthDepositVault - * @notice Smart contract that handles mEVETH minting - * @author RedDuck Software - */ -contract MEvEthDepositVault is DepositVault, MEvEthMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_EV_ETH_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEVETH/MEvEthMidasAccessControlRoles.sol b/contracts/products/mEVETH/MEvEthMidasAccessControlRoles.sol deleted file mode 100644 index 91d5a002..00000000 --- a/contracts/products/mEVETH/MEvEthMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MEvEthMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mEVETH contracts - * @author RedDuck Software - */ -abstract contract MEvEthMidasAccessControlRoles { - /** - * @notice actor that can manage MEvEthDepositVault - */ - bytes32 public constant M_EV_ETH_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_EV_ETH_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MEvEthRedemptionVault - */ - bytes32 public constant M_EV_ETH_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_EV_ETH_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MEvEthCustomAggregatorFeed and MEvEthDataFeed - */ - bytes32 public constant M_EV_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_EV_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mEVETH/MEvEthRedemptionVaultWithSwapper.sol b/contracts/products/mEVETH/MEvEthRedemptionVaultWithSwapper.sol deleted file mode 100644 index 8cbe367c..00000000 --- a/contracts/products/mEVETH/MEvEthRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MEvEthMidasAccessControlRoles.sol"; - -/** - * @title MEvEthRedemptionVaultWithSwapper - * @notice Smart contract that handles mEVETH redemptions - * @author RedDuck Software - */ -contract MEvEthRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MEvEthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_EV_ETH_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEVETH/mEVETH.sol b/contracts/products/mEVETH/mEVETH.sol deleted file mode 100644 index 42843369..00000000 --- a/contracts/products/mEVETH/mEVETH.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mEVETH - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mEVETH is mToken { - /** - * @notice actor that can mint mEVETH - */ - bytes32 public constant M_EV_ETH_MINT_OPERATOR_ROLE = - keccak256("M_EV_ETH_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mEVETH - */ - bytes32 public constant M_EV_ETH_BURN_OPERATOR_ROLE = - keccak256("M_EV_ETH_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mEVETH - */ - bytes32 public constant M_EV_ETH_PAUSE_OPERATOR_ROLE = - keccak256("M_EV_ETH_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Everstake ETH", "mEVETH"); - } - - /** - * @dev AC role, owner of which can mint mEVETH token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_EV_ETH_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mEVETH token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_EV_ETH_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mEVETH token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_EV_ETH_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mEVUSD/MEvUsdCustomAggregatorFeed.sol b/contracts/products/mEVUSD/MEvUsdCustomAggregatorFeed.sol deleted file mode 100644 index d7355aec..00000000 --- a/contracts/products/mEVUSD/MEvUsdCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MEvUsdMidasAccessControlRoles.sol"; - -/** - * @title MEvUsdCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mEVUSD, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MEvUsdCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MEvUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_EV_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEVUSD/MEvUsdDataFeed.sol b/contracts/products/mEVUSD/MEvUsdDataFeed.sol deleted file mode 100644 index f20b9dd4..00000000 --- a/contracts/products/mEVUSD/MEvUsdDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MEvUsdMidasAccessControlRoles.sol"; - -/** - * @title MEvUsdDataFeed - * @notice DataFeed for mEVUSD product - * @author RedDuck Software - */ -contract MEvUsdDataFeed is DataFeed, MEvUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_EV_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEVUSD/MEvUsdDepositVault.sol b/contracts/products/mEVUSD/MEvUsdDepositVault.sol deleted file mode 100644 index ab7d84fb..00000000 --- a/contracts/products/mEVUSD/MEvUsdDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MEvUsdMidasAccessControlRoles.sol"; - -/** - * @title MEvUsdDepositVault - * @notice Smart contract that handles mEVUSD minting - * @author RedDuck Software - */ -contract MEvUsdDepositVault is DepositVault, MEvUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_EV_USD_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEVUSD/MEvUsdMidasAccessControlRoles.sol b/contracts/products/mEVUSD/MEvUsdMidasAccessControlRoles.sol deleted file mode 100644 index 128f44fe..00000000 --- a/contracts/products/mEVUSD/MEvUsdMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MEvUsdMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mEVUSD contracts - * @author RedDuck Software - */ -abstract contract MEvUsdMidasAccessControlRoles { - /** - * @notice actor that can manage MEvUsdDepositVault - */ - bytes32 public constant M_EV_USD_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_EV_USD_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MEvUsdRedemptionVault - */ - bytes32 public constant M_EV_USD_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_EV_USD_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MEvUsdCustomAggregatorFeed and MEvUsdDataFeed - */ - bytes32 public constant M_EV_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_EV_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mEVUSD/MEvUsdRedemptionVaultWithSwapper.sol b/contracts/products/mEVUSD/MEvUsdRedemptionVaultWithSwapper.sol deleted file mode 100644 index fe193c3c..00000000 --- a/contracts/products/mEVUSD/MEvUsdRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MEvUsdMidasAccessControlRoles.sol"; - -/** - * @title MEvUsdRedemptionVaultWithSwapper - * @notice Smart contract that handles mEVUSD redemptions - * @author RedDuck Software - */ -contract MEvUsdRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MEvUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_EV_USD_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mEVUSD/mEVUSD.sol b/contracts/products/mEVUSD/mEVUSD.sol deleted file mode 100644 index e347176d..00000000 --- a/contracts/products/mEVUSD/mEVUSD.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mEVUSD - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mEVUSD is mToken { - /** - * @notice actor that can mint mEVUSD - */ - bytes32 public constant M_EV_USD_MINT_OPERATOR_ROLE = - keccak256("M_EV_USD_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mEVUSD - */ - bytes32 public constant M_EV_USD_BURN_OPERATOR_ROLE = - keccak256("M_EV_USD_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mEVUSD - */ - bytes32 public constant M_EV_USD_PAUSE_OPERATOR_ROLE = - keccak256("M_EV_USD_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Everstake USD", "mEVUSD"); - } - - /** - * @dev AC role, owner of which can mint mEVUSD token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_EV_USD_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mEVUSD token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_EV_USD_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mEVUSD token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_EV_USD_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mFARM/MFarmCustomAggregatorFeed.sol b/contracts/products/mFARM/MFarmCustomAggregatorFeed.sol deleted file mode 100644 index 69827c7a..00000000 --- a/contracts/products/mFARM/MFarmCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MFarmMidasAccessControlRoles.sol"; - -/** - * @title MFarmCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mFARM, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MFarmCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MFarmMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_FARM_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mFARM/MFarmDataFeed.sol b/contracts/products/mFARM/MFarmDataFeed.sol deleted file mode 100644 index 4133b343..00000000 --- a/contracts/products/mFARM/MFarmDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MFarmMidasAccessControlRoles.sol"; - -/** - * @title MFarmDataFeed - * @notice DataFeed for mFARM product - * @author RedDuck Software - */ -contract MFarmDataFeed is DataFeed, MFarmMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_FARM_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mFARM/MFarmDepositVault.sol b/contracts/products/mFARM/MFarmDepositVault.sol deleted file mode 100644 index 41ac65bf..00000000 --- a/contracts/products/mFARM/MFarmDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MFarmMidasAccessControlRoles.sol"; - -/** - * @title MFarmDepositVault - * @notice Smart contract that handles mFARM minting - * @author RedDuck Software - */ -contract MFarmDepositVault is DepositVault, MFarmMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_FARM_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mFARM/MFarmMidasAccessControlRoles.sol b/contracts/products/mFARM/MFarmMidasAccessControlRoles.sol deleted file mode 100644 index 96c758cf..00000000 --- a/contracts/products/mFARM/MFarmMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MFarmMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mFARM contracts - * @author RedDuck Software - */ -abstract contract MFarmMidasAccessControlRoles { - /** - * @notice actor that can manage MFarmDepositVault - */ - bytes32 public constant M_FARM_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_FARM_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MFarmRedemptionVault - */ - bytes32 public constant M_FARM_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_FARM_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MFarmCustomAggregatorFeed and MFarmDataFeed - */ - bytes32 public constant M_FARM_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_FARM_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mFARM/MFarmRedemptionVaultWithSwapper.sol b/contracts/products/mFARM/MFarmRedemptionVaultWithSwapper.sol deleted file mode 100644 index 7e5d4c53..00000000 --- a/contracts/products/mFARM/MFarmRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MFarmMidasAccessControlRoles.sol"; - -/** - * @title MFarmRedemptionVaultWithSwapper - * @notice Smart contract that handles mFARM redemptions - * @author RedDuck Software - */ -contract MFarmRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MFarmMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_FARM_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mFARM/mFARM.sol b/contracts/products/mFARM/mFARM.sol deleted file mode 100644 index a0d9ec09..00000000 --- a/contracts/products/mFARM/mFARM.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mFARM - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mFARM is mToken { - /** - * @notice actor that can mint mFARM - */ - bytes32 public constant M_FARM_MINT_OPERATOR_ROLE = - keccak256("M_FARM_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mFARM - */ - bytes32 public constant M_FARM_BURN_OPERATOR_ROLE = - keccak256("M_FARM_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mFARM - */ - bytes32 public constant M_FARM_PAUSE_OPERATOR_ROLE = - keccak256("M_FARM_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Farm Capital", "mFARM"); - } - - /** - * @dev AC role, owner of which can mint mFARM token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_FARM_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mFARM token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_FARM_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mFARM token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_FARM_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mFONE/MFOneCustomAggregatorFeed.sol b/contracts/products/mFONE/MFOneCustomAggregatorFeed.sol deleted file mode 100644 index bdf7535c..00000000 --- a/contracts/products/mFONE/MFOneCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MFOneMidasAccessControlRoles.sol"; - -/** - * @title MFOneCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mF-ONE, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MFOneCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MFOneMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_FONE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mFONE/MFOneDataFeed.sol b/contracts/products/mFONE/MFOneDataFeed.sol deleted file mode 100644 index e6775650..00000000 --- a/contracts/products/mFONE/MFOneDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MFOneMidasAccessControlRoles.sol"; - -/** - * @title MFOneDataFeed - * @notice DataFeed for mF-ONE product - * @author RedDuck Software - */ -contract MFOneDataFeed is DataFeed, MFOneMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_FONE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mFONE/MFOneDepositVault.sol b/contracts/products/mFONE/MFOneDepositVault.sol deleted file mode 100644 index d97b6eb6..00000000 --- a/contracts/products/mFONE/MFOneDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MFOneMidasAccessControlRoles.sol"; - -/** - * @title MFOneDepositVault - * @notice Smart contract that handles mF-ONE minting - * @author RedDuck Software - */ -contract MFOneDepositVault is DepositVault, MFOneMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_FONE_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mFONE/MFOneMidasAccessControlRoles.sol b/contracts/products/mFONE/MFOneMidasAccessControlRoles.sol deleted file mode 100644 index 8d4d2396..00000000 --- a/contracts/products/mFONE/MFOneMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MFOneMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mF-ONE contracts - * @author RedDuck Software - */ -abstract contract MFOneMidasAccessControlRoles { - /** - * @notice actor that can manage MFOneDepositVault - */ - bytes32 public constant M_FONE_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_FONE_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MFOneRedemptionVault - */ - bytes32 public constant M_FONE_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_FONE_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MFOneCustomAggregatorFeed and MFOneDataFeed - */ - bytes32 public constant M_FONE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_FONE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mFONE/MFOneRedemptionVaultWithMToken.sol b/contracts/products/mFONE/MFOneRedemptionVaultWithMToken.sol deleted file mode 100644 index 943efbbf..00000000 --- a/contracts/products/mFONE/MFOneRedemptionVaultWithMToken.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithMToken.sol"; -import "./MFOneMidasAccessControlRoles.sol"; - -/** - * @title MFOneRedemptionVaultWithMToken - * @notice Smart contract that handles mF-ONE redemptions using mToken - * liquid strategy. Upgrade-compatible replacement for - * MFOneRedemptionVaultWithSwapper. - * @author RedDuck Software - */ -contract MFOneRedemptionVaultWithMToken is - RedemptionVaultWithMToken, - MFOneMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_FONE_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mFONE/MFOneRedemptionVaultWithSwapper.sol b/contracts/products/mFONE/MFOneRedemptionVaultWithSwapper.sol deleted file mode 100644 index 4e9cf402..00000000 --- a/contracts/products/mFONE/MFOneRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MFOneMidasAccessControlRoles.sol"; - -/** - * @title MFOneRedemptionVaultWithSwapper - * @notice Smart contract that handles mF-ONE redemptions - * @author RedDuck Software - */ -contract MFOneRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MFOneMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_FONE_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mFONE/mFONE.sol b/contracts/products/mFONE/mFONE.sol deleted file mode 100644 index 2516a3c3..00000000 --- a/contracts/products/mFONE/mFONE.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title mF-ONE - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mFONE is mToken { - /** - * @notice actor that can mint mF-ONE - */ - bytes32 public constant M_FONE_MINT_OPERATOR_ROLE = - keccak256("M_FONE_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mF-ONE - */ - bytes32 public constant M_FONE_BURN_OPERATOR_ROLE = - keccak256("M_FONE_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mF-ONE - */ - bytes32 public constant M_FONE_PAUSE_OPERATOR_ROLE = - keccak256("M_FONE_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Fasanara ONE", "mF-ONE"); - } - - /** - * @dev AC role, owner of which can mint mF-ONE token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_FONE_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mF-ONE token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_FONE_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mF-ONE token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_FONE_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mGLO/MGloCustomAggregatorFeed.sol b/contracts/products/mGLO/MGloCustomAggregatorFeed.sol deleted file mode 100644 index 6d4ed57b..00000000 --- a/contracts/products/mGLO/MGloCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MGloMidasAccessControlRoles.sol"; - -/** - * @title MGloCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mGLO, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MGloCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MGloMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_GLO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mGLO/MGloDataFeed.sol b/contracts/products/mGLO/MGloDataFeed.sol deleted file mode 100644 index af0f5a74..00000000 --- a/contracts/products/mGLO/MGloDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MGloMidasAccessControlRoles.sol"; - -/** - * @title MGloDataFeed - * @notice DataFeed for mGLO product - * @author RedDuck Software - */ -contract MGloDataFeed is DataFeed, MGloMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_GLO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mGLO/MGloDepositVault.sol b/contracts/products/mGLO/MGloDepositVault.sol deleted file mode 100644 index 52864442..00000000 --- a/contracts/products/mGLO/MGloDepositVault.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MGloMidasAccessControlRoles.sol"; - -/** - * @title MGloDepositVault - * @notice Smart contract that handles mGLO minting - * @author RedDuck Software - */ -contract MGloDepositVault is DepositVault, MGloMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_GLO_DEPOSIT_VAULT_ADMIN_ROLE; - } - - /** - * @inheritdoc Greenlistable - */ - function greenlistedRole() public pure override returns (bytes32) { - return M_GLOBAL_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/mGLO/MGloMidasAccessControlRoles.sol b/contracts/products/mGLO/MGloMidasAccessControlRoles.sol deleted file mode 100644 index 7df4d865..00000000 --- a/contracts/products/mGLO/MGloMidasAccessControlRoles.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MGloMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mGLO contracts - * @author RedDuck Software - */ -abstract contract MGloMidasAccessControlRoles { - /** - * @notice actor that can manage MGloDepositVault - */ - bytes32 public constant M_GLO_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_GLO_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MGloRedemptionVault - */ - bytes32 public constant M_GLO_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_GLO_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MGloCustomAggregatorFeed and MGloDataFeed - */ - bytes32 public constant M_GLO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_GLO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); - - /** - * @notice greenlist role for mGLO - */ - bytes32 public constant M_GLOBAL_GREENLISTED_ROLE = - keccak256("M_GLOBAL_GREENLISTED_ROLE"); -} diff --git a/contracts/products/mGLO/MGloRedemptionVaultWithSwapper.sol b/contracts/products/mGLO/MGloRedemptionVaultWithSwapper.sol deleted file mode 100644 index 01b84ab3..00000000 --- a/contracts/products/mGLO/MGloRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MGloMidasAccessControlRoles.sol"; - -/** - * @title MGloRedemptionVaultWithSwapper - * @notice Smart contract that handles mGLO redemptions - * @author RedDuck Software - */ -contract MGloRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MGloMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_GLO_REDEMPTION_VAULT_ADMIN_ROLE; - } - - /** - * @inheritdoc Greenlistable - */ - function greenlistedRole() public pure override returns (bytes32) { - return M_GLOBAL_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/mGLO/mGLO.sol b/contracts/products/mGLO/mGLO.sol deleted file mode 100644 index 4ba65cf8..00000000 --- a/contracts/products/mGLO/mGLO.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mGLO - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mGLO is mToken { - /** - * @notice actor that can mint mGLO - */ - bytes32 public constant M_GLO_MINT_OPERATOR_ROLE = - keccak256("M_GLO_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mGLO - */ - bytes32 public constant M_GLO_BURN_OPERATOR_ROLE = - keccak256("M_GLO_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mGLO - */ - bytes32 public constant M_GLO_PAUSE_OPERATOR_ROLE = - keccak256("M_GLO_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Fasanara Global Open", "mGLO"); - } - - /** - * @dev AC role, owner of which can mint mGLO token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_GLO_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mGLO token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_GLO_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mGLO token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_GLO_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mGLOBAL/MGlobalCustomAggregatorFeedGrowth.sol b/contracts/products/mGLOBAL/MGlobalCustomAggregatorFeedGrowth.sol deleted file mode 100644 index 7fc09a23..00000000 --- a/contracts/products/mGLOBAL/MGlobalCustomAggregatorFeedGrowth.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeedGrowth.sol"; -import "./MGlobalMidasAccessControlRoles.sol"; - -/** - * @title MGlobalCustomAggregatorFeedGrowth - * @notice AggregatorV3 compatible feed for mGLOBAL, - * where price is submitted manually by feed admins, - * and growth apr applies to the answer. - * @author RedDuck Software - */ -contract MGlobalCustomAggregatorFeedGrowth is - CustomAggregatorV3CompatibleFeedGrowth, - MGlobalMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeedGrowth - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_GLOBAL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mGLOBAL/MGlobalDataFeed.sol b/contracts/products/mGLOBAL/MGlobalDataFeed.sol deleted file mode 100644 index cafba33a..00000000 --- a/contracts/products/mGLOBAL/MGlobalDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MGlobalMidasAccessControlRoles.sol"; - -/** - * @title MGlobalDataFeed - * @notice DataFeed for mGLOBAL product - * @author RedDuck Software - */ -contract MGlobalDataFeed is DataFeed, MGlobalMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_GLOBAL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mGLOBAL/MGlobalDepositVaultWithAave.sol b/contracts/products/mGLOBAL/MGlobalDepositVaultWithAave.sol deleted file mode 100644 index f8d42b59..00000000 --- a/contracts/products/mGLOBAL/MGlobalDepositVaultWithAave.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVaultWithAave.sol"; -import "./MGlobalMidasAccessControlRoles.sol"; - -/** - * @title MGlobalDepositVaultWithAave - * @notice Smart contract that handles mGLOBAL minting with Aave V3 auto-invest - * @author RedDuck Software - */ -contract MGlobalDepositVaultWithAave is - DepositVaultWithAave, - MGlobalMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_GLOBAL_DEPOSIT_VAULT_ADMIN_ROLE; - } - - /** - * @inheritdoc Greenlistable - */ - function greenlistedRole() public pure override returns (bytes32) { - return M_GLOBAL_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/mGLOBAL/MGlobalInfiniFiCustomAggregatorFeedGrowth.sol b/contracts/products/mGLOBAL/MGlobalInfiniFiCustomAggregatorFeedGrowth.sol deleted file mode 100644 index 7880f088..00000000 --- a/contracts/products/mGLOBAL/MGlobalInfiniFiCustomAggregatorFeedGrowth.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeedGrowth.sol"; -import "./MGlobalMidasAccessControlRoles.sol"; - -/** - * @title MGlobalInfiniFiCustomAggregatorFeedGrowth - * @notice AggregatorV3 compatible feed for mGLOBAL dedicated to the InfiniFi - * integration, where price is submitted manually by feed admins, - * and growth apr applies to the answer. - * @author RedDuck Software - */ -contract MGlobalInfiniFiCustomAggregatorFeedGrowth is - CustomAggregatorV3CompatibleFeedGrowth, - MGlobalMidasAccessControlRoles -{ - /** - * @notice feed admin for this InfiniFi oracle only — not shared with core mGLOBAL role mixins. - */ - bytes32 public constant INFINIFI_MG_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("INFINIFI_MG_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeedGrowth - */ - function feedAdminRole() public pure override returns (bytes32) { - return INFINIFI_MG_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mGLOBAL/MGlobalMidasAccessControlRoles.sol b/contracts/products/mGLOBAL/MGlobalMidasAccessControlRoles.sol deleted file mode 100644 index 561d0ea6..00000000 --- a/contracts/products/mGLOBAL/MGlobalMidasAccessControlRoles.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MGlobalMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mGLOBAL contracts - * @author RedDuck Software - */ -abstract contract MGlobalMidasAccessControlRoles { - /** - * @notice actor that can manage MGlobalDepositVault - */ - bytes32 public constant M_GLOBAL_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_GLOBAL_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MGlobalRedemptionVault - */ - bytes32 public constant M_GLOBAL_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_GLOBAL_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MGlobalCustomAggregatorFeed and MGlobalDataFeed - */ - bytes32 public constant M_GLOBAL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_GLOBAL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); - - /** - * @notice greenlist role for mGLOBAL - */ - bytes32 public constant M_GLOBAL_GREENLISTED_ROLE = - keccak256("M_GLOBAL_GREENLISTED_ROLE"); -} diff --git a/contracts/products/mGLOBAL/MGlobalRedemptionVaultWithAave.sol b/contracts/products/mGLOBAL/MGlobalRedemptionVaultWithAave.sol deleted file mode 100644 index 17f19dbb..00000000 --- a/contracts/products/mGLOBAL/MGlobalRedemptionVaultWithAave.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithAave.sol"; -import "./MGlobalMidasAccessControlRoles.sol"; - -/** - * @title MGlobalRedemptionVaultWithAave - * @notice Smart contract that handles mGLOBAL redemptions via Aave V3 - * @author RedDuck Software - */ -contract MGlobalRedemptionVaultWithAave is - RedemptionVaultWithAave, - MGlobalMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_GLOBAL_REDEMPTION_VAULT_ADMIN_ROLE; - } - - /** - * @inheritdoc Greenlistable - */ - function greenlistedRole() public pure override returns (bytes32) { - return M_GLOBAL_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/mGLOBAL/MGlobalRedemptionVaultWithSwapper.sol b/contracts/products/mGLOBAL/MGlobalRedemptionVaultWithSwapper.sol deleted file mode 100644 index 88f68ef1..00000000 --- a/contracts/products/mGLOBAL/MGlobalRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MGlobalMidasAccessControlRoles.sol"; - -/** - * @title MGlobalRedemptionVaultWithSwapper - * @notice Smart contract that handles mGLOBAL redemptions - * @author RedDuck Software - */ -contract MGlobalRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MGlobalMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_GLOBAL_REDEMPTION_VAULT_ADMIN_ROLE; - } - - /** - * @inheritdoc Greenlistable - */ - function greenlistedRole() public pure override returns (bytes32) { - return M_GLOBAL_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/mGLOBAL/mGLOBAL.sol b/contracts/products/mGLOBAL/mGLOBAL.sol deleted file mode 100644 index 626639b4..00000000 --- a/contracts/products/mGLOBAL/mGLOBAL.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mTokenPermissioned.sol"; -import "./MGlobalMidasAccessControlRoles.sol"; - -/** - * @title mGLOBAL - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mGLOBAL is mTokenPermissioned, MGlobalMidasAccessControlRoles { - /** - * @notice actor that can mint mGLOBAL - */ - bytes32 public constant M_GLOBAL_MINT_OPERATOR_ROLE = - keccak256("M_GLOBAL_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mGLOBAL - */ - bytes32 public constant M_GLOBAL_BURN_OPERATOR_ROLE = - keccak256("M_GLOBAL_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mGLOBAL - */ - bytes32 public constant M_GLOBAL_PAUSE_OPERATOR_ROLE = - keccak256("M_GLOBAL_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Fasanara Global", "mGLOBAL"); - } - - /** - * @dev AC role, owner of which can mint mGLOBAL token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_GLOBAL_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mGLOBAL token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_GLOBAL_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mGLOBAL token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_GLOBAL_PAUSE_OPERATOR_ROLE; - } - - /** - * @inheritdoc mTokenPermissioned - */ - function _greenlistedRole() internal pure override returns (bytes32) { - return M_GLOBAL_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/mHYPER/MHyperCustomAggregatorFeed.sol b/contracts/products/mHYPER/MHyperCustomAggregatorFeed.sol deleted file mode 100644 index 13ab8841..00000000 --- a/contracts/products/mHYPER/MHyperCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MHyperMidasAccessControlRoles.sol"; - -/** - * @title MHyperCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mHYPER, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MHyperCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MHyperMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_HYPER_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mHYPER/MHyperDataFeed.sol b/contracts/products/mHYPER/MHyperDataFeed.sol deleted file mode 100644 index c48ffa73..00000000 --- a/contracts/products/mHYPER/MHyperDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MHyperMidasAccessControlRoles.sol"; - -/** - * @title MHyperDataFeed - * @notice DataFeed for mHYPER product - * @author RedDuck Software - */ -contract MHyperDataFeed is DataFeed, MHyperMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_HYPER_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mHYPER/MHyperDepositVault.sol b/contracts/products/mHYPER/MHyperDepositVault.sol deleted file mode 100644 index e597d7e1..00000000 --- a/contracts/products/mHYPER/MHyperDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MHyperMidasAccessControlRoles.sol"; - -/** - * @title MHyperDepositVault - * @notice Smart contract that handles mHYPER minting - * @author RedDuck Software - */ -contract MHyperDepositVault is DepositVault, MHyperMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_HYPER_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mHYPER/MHyperMidasAccessControlRoles.sol b/contracts/products/mHYPER/MHyperMidasAccessControlRoles.sol deleted file mode 100644 index c9d13298..00000000 --- a/contracts/products/mHYPER/MHyperMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MHyperMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mHYPER contracts - * @author RedDuck Software - */ -abstract contract MHyperMidasAccessControlRoles { - /** - * @notice actor that can manage MHyperDepositVault - */ - bytes32 public constant M_HYPER_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_HYPER_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MHyperRedemptionVault - */ - bytes32 public constant M_HYPER_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_HYPER_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MHyperCustomAggregatorFeed and MHyperDataFeed - */ - bytes32 public constant M_HYPER_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_HYPER_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mHYPER/MHyperRedemptionVaultWithSwapper.sol b/contracts/products/mHYPER/MHyperRedemptionVaultWithSwapper.sol deleted file mode 100644 index 085ae9bd..00000000 --- a/contracts/products/mHYPER/MHyperRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MHyperMidasAccessControlRoles.sol"; - -/** - * @title MHyperRedemptionVaultWithSwapper - * @notice Smart contract that handles mHYPER redemptions - * @author RedDuck Software - */ -contract MHyperRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MHyperMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_HYPER_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mHYPER/mHYPER.sol b/contracts/products/mHYPER/mHYPER.sol deleted file mode 100644 index 4b15d5a2..00000000 --- a/contracts/products/mHYPER/mHYPER.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title mHYPER - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mHYPER is mToken { - /** - * @notice actor that can mint mHYPER - */ - bytes32 public constant M_HYPER_MINT_OPERATOR_ROLE = - keccak256("M_HYPER_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mHYPER - */ - bytes32 public constant M_HYPER_BURN_OPERATOR_ROLE = - keccak256("M_HYPER_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mHYPER - */ - bytes32 public constant M_HYPER_PAUSE_OPERATOR_ROLE = - keccak256("M_HYPER_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Hyperithm", "mHYPER"); - } - - /** - * @dev AC role, owner of which can mint mHYPER token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_HYPER_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mHYPER token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_HYPER_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mHYPER token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_HYPER_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mHyperBTC/MHyperBtcCustomAggregatorFeed.sol b/contracts/products/mHyperBTC/MHyperBtcCustomAggregatorFeed.sol deleted file mode 100644 index 2428096d..00000000 --- a/contracts/products/mHyperBTC/MHyperBtcCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MHyperBtcMidasAccessControlRoles.sol"; - -/** - * @title MHyperBtcCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mHyperBTC, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MHyperBtcCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MHyperBtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_HYPER_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mHyperBTC/MHyperBtcDataFeed.sol b/contracts/products/mHyperBTC/MHyperBtcDataFeed.sol deleted file mode 100644 index 045515a7..00000000 --- a/contracts/products/mHyperBTC/MHyperBtcDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MHyperBtcMidasAccessControlRoles.sol"; - -/** - * @title MHyperBtcDataFeed - * @notice DataFeed for mHyperBTC product - * @author RedDuck Software - */ -contract MHyperBtcDataFeed is DataFeed, MHyperBtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_HYPER_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mHyperBTC/MHyperBtcDepositVault.sol b/contracts/products/mHyperBTC/MHyperBtcDepositVault.sol deleted file mode 100644 index d48a1045..00000000 --- a/contracts/products/mHyperBTC/MHyperBtcDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MHyperBtcMidasAccessControlRoles.sol"; - -/** - * @title MHyperBtcDepositVault - * @notice Smart contract that handles mHyperBTC minting - * @author RedDuck Software - */ -contract MHyperBtcDepositVault is - DepositVault, - MHyperBtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_HYPER_BTC_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mHyperBTC/MHyperBtcMidasAccessControlRoles.sol b/contracts/products/mHyperBTC/MHyperBtcMidasAccessControlRoles.sol deleted file mode 100644 index e044aa99..00000000 --- a/contracts/products/mHyperBTC/MHyperBtcMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MHyperBtcMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mHyperBTC contracts - * @author RedDuck Software - */ -abstract contract MHyperBtcMidasAccessControlRoles { - /** - * @notice actor that can manage MHyperBtcDepositVault - */ - bytes32 public constant M_HYPER_BTC_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_HYPER_BTC_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MHyperBtcRedemptionVault - */ - bytes32 public constant M_HYPER_BTC_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_HYPER_BTC_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MHyperBtcCustomAggregatorFeed and MHyperBtcDataFeed - */ - bytes32 public constant M_HYPER_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_HYPER_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mHyperBTC/MHyperBtcRedemptionVaultWithSwapper.sol b/contracts/products/mHyperBTC/MHyperBtcRedemptionVaultWithSwapper.sol deleted file mode 100644 index 9a8165df..00000000 --- a/contracts/products/mHyperBTC/MHyperBtcRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MHyperBtcMidasAccessControlRoles.sol"; - -/** - * @title MHyperBtcRedemptionVaultWithSwapper - * @notice Smart contract that handles mHyperBTC redemptions - * @author RedDuck Software - */ -contract MHyperBtcRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MHyperBtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_HYPER_BTC_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mHyperBTC/mHyperBTC.sol b/contracts/products/mHyperBTC/mHyperBTC.sol deleted file mode 100644 index 856f6ced..00000000 --- a/contracts/products/mHyperBTC/mHyperBTC.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mHyperBTC - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mHyperBTC is mToken { - /** - * @notice actor that can mint mHyperBTC - */ - bytes32 public constant M_HYPER_BTC_MINT_OPERATOR_ROLE = - keccak256("M_HYPER_BTC_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mHyperBTC - */ - bytes32 public constant M_HYPER_BTC_BURN_OPERATOR_ROLE = - keccak256("M_HYPER_BTC_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mHyperBTC - */ - bytes32 public constant M_HYPER_BTC_PAUSE_OPERATOR_ROLE = - keccak256("M_HYPER_BTC_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Hyperithm BTC", "mHyperBTC"); - } - - /** - * @dev AC role, owner of which can mint mHyperBTC token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_HYPER_BTC_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mHyperBTC token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_HYPER_BTC_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mHyperBTC token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_HYPER_BTC_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mHyperETH/MHyperEthCustomAggregatorFeed.sol b/contracts/products/mHyperETH/MHyperEthCustomAggregatorFeed.sol deleted file mode 100644 index 6084afc5..00000000 --- a/contracts/products/mHyperETH/MHyperEthCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MHyperEthMidasAccessControlRoles.sol"; - -/** - * @title MHyperEthCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mHyperETH, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MHyperEthCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MHyperEthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_HYPER_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mHyperETH/MHyperEthDataFeed.sol b/contracts/products/mHyperETH/MHyperEthDataFeed.sol deleted file mode 100644 index 67175d43..00000000 --- a/contracts/products/mHyperETH/MHyperEthDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MHyperEthMidasAccessControlRoles.sol"; - -/** - * @title MHyperEthDataFeed - * @notice DataFeed for mHyperETH product - * @author RedDuck Software - */ -contract MHyperEthDataFeed is DataFeed, MHyperEthMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_HYPER_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mHyperETH/MHyperEthDepositVault.sol b/contracts/products/mHyperETH/MHyperEthDepositVault.sol deleted file mode 100644 index cc322735..00000000 --- a/contracts/products/mHyperETH/MHyperEthDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MHyperEthMidasAccessControlRoles.sol"; - -/** - * @title MHyperEthDepositVault - * @notice Smart contract that handles mHyperETH minting - * @author RedDuck Software - */ -contract MHyperEthDepositVault is - DepositVault, - MHyperEthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_HYPER_ETH_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mHyperETH/MHyperEthMidasAccessControlRoles.sol b/contracts/products/mHyperETH/MHyperEthMidasAccessControlRoles.sol deleted file mode 100644 index f76c5354..00000000 --- a/contracts/products/mHyperETH/MHyperEthMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MHyperEthMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mHyperETH contracts - * @author RedDuck Software - */ -abstract contract MHyperEthMidasAccessControlRoles { - /** - * @notice actor that can manage MHyperEthDepositVault - */ - bytes32 public constant M_HYPER_ETH_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_HYPER_ETH_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MHyperEthRedemptionVault - */ - bytes32 public constant M_HYPER_ETH_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_HYPER_ETH_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MHyperEthCustomAggregatorFeed and MHyperEthDataFeed - */ - bytes32 public constant M_HYPER_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_HYPER_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mHyperETH/MHyperEthRedemptionVaultWithSwapper.sol b/contracts/products/mHyperETH/MHyperEthRedemptionVaultWithSwapper.sol deleted file mode 100644 index 144c24fe..00000000 --- a/contracts/products/mHyperETH/MHyperEthRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MHyperEthMidasAccessControlRoles.sol"; - -/** - * @title MHyperEthRedemptionVaultWithSwapper - * @notice Smart contract that handles mHyperETH redemptions - * @author RedDuck Software - */ -contract MHyperEthRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MHyperEthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_HYPER_ETH_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mHyperETH/mHyperETH.sol b/contracts/products/mHyperETH/mHyperETH.sol deleted file mode 100644 index 71e053cc..00000000 --- a/contracts/products/mHyperETH/mHyperETH.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mHyperETH - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mHyperETH is mToken { - /** - * @notice actor that can mint mHyperETH - */ - bytes32 public constant M_HYPER_ETH_MINT_OPERATOR_ROLE = - keccak256("M_HYPER_ETH_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mHyperETH - */ - bytes32 public constant M_HYPER_ETH_BURN_OPERATOR_ROLE = - keccak256("M_HYPER_ETH_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mHyperETH - */ - bytes32 public constant M_HYPER_ETH_PAUSE_OPERATOR_ROLE = - keccak256("M_HYPER_ETH_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Hyperithm ETH", "mHyperETH"); - } - - /** - * @dev AC role, owner of which can mint mHyperETH token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_HYPER_ETH_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mHyperETH token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_HYPER_ETH_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mHyperETH token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_HYPER_ETH_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mKRalpha/MKRalphaCustomAggregatorFeed.sol b/contracts/products/mKRalpha/MKRalphaCustomAggregatorFeed.sol deleted file mode 100644 index 39c1c4a8..00000000 --- a/contracts/products/mKRalpha/MKRalphaCustomAggregatorFeed.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MKRalphaMidasAccessControlRoles.sol"; - -/** - * @title MKRalphaCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mKRalpha, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MKRalphaCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MKRalphaMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function initialize( - address _accessControl, - int192 _minAnswer, - int192 _maxAnswer, - uint256 _maxAnswerDeviation, - string calldata _description - ) public override { - super.initialize( - _accessControl, - _minAnswer, - _maxAnswer, - _maxAnswerDeviation, - _description - ); - // call v2 to increase contract version to 2 - initializeV2(_maxAnswerDeviation); - } - - /** - * @notice initializes the contract with a new max answer deviation - * @dev increases contract version to 2 - * @param _newMaxAnswerDeviation new max answer deviation - */ - function initializeV2(uint256 _newMaxAnswerDeviation) - public - reinitializer(2) - { - require( - _newMaxAnswerDeviation <= 100 * (10**decimals()), - "CA: !max deviation" - ); - - maxAnswerDeviation = _newMaxAnswerDeviation; - } - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_KRALPHA_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mKRalpha/MKRalphaDataFeed.sol b/contracts/products/mKRalpha/MKRalphaDataFeed.sol deleted file mode 100644 index 69814c70..00000000 --- a/contracts/products/mKRalpha/MKRalphaDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MKRalphaMidasAccessControlRoles.sol"; - -/** - * @title MKRalphaDataFeed - * @notice DataFeed for mKRalpha product - * @author RedDuck Software - */ -contract MKRalphaDataFeed is DataFeed, MKRalphaMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_KRALPHA_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mKRalpha/MKRalphaDepositVault.sol b/contracts/products/mKRalpha/MKRalphaDepositVault.sol deleted file mode 100644 index e3e52687..00000000 --- a/contracts/products/mKRalpha/MKRalphaDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MKRalphaMidasAccessControlRoles.sol"; - -/** - * @title MKRalphaDepositVault - * @notice Smart contract that handles mKRalpha minting - * @author RedDuck Software - */ -contract MKRalphaDepositVault is DepositVault, MKRalphaMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_KRALPHA_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mKRalpha/MKRalphaMidasAccessControlRoles.sol b/contracts/products/mKRalpha/MKRalphaMidasAccessControlRoles.sol deleted file mode 100644 index 0d982458..00000000 --- a/contracts/products/mKRalpha/MKRalphaMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MKRalphaMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mKRalpha contracts - * @author RedDuck Software - */ -abstract contract MKRalphaMidasAccessControlRoles { - /** - * @notice actor that can manage MKRalphaDepositVault - */ - bytes32 public constant M_KRALPHA_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_KRALPHA_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MKRalphaRedemptionVault - */ - bytes32 public constant M_KRALPHA_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_KRALPHA_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MKRalphaCustomAggregatorFeed and MKRalphaDataFeed - */ - bytes32 public constant M_KRALPHA_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_KRALPHA_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mKRalpha/MKRalphaRedemptionVaultWithSwapper.sol b/contracts/products/mKRalpha/MKRalphaRedemptionVaultWithSwapper.sol deleted file mode 100644 index 705fdda1..00000000 --- a/contracts/products/mKRalpha/MKRalphaRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MKRalphaMidasAccessControlRoles.sol"; - -/** - * @title MKRalphaRedemptionVaultWithSwapper - * @notice Smart contract that handles mKRalpha redemptions - * @author RedDuck Software - */ -contract MKRalphaRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MKRalphaMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_KRALPHA_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mKRalpha/mKRalpha.sol b/contracts/products/mKRalpha/mKRalpha.sol deleted file mode 100644 index 5b6c98fd..00000000 --- a/contracts/products/mKRalpha/mKRalpha.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mKRalpha - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mKRalpha is mToken { - /** - * @notice actor that can mint mKRalpha - */ - bytes32 public constant M_KRALPHA_MINT_OPERATOR_ROLE = - keccak256("M_KRALPHA_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mKRalpha - */ - bytes32 public constant M_KRALPHA_BURN_OPERATOR_ROLE = - keccak256("M_KRALPHA_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mKRalpha - */ - bytes32 public constant M_KRALPHA_PAUSE_OPERATOR_ROLE = - keccak256("M_KRALPHA_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Keyrock Alpha", "mKRalpha"); - } - - /** - * @dev AC role, owner of which can mint mKRalpha token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_KRALPHA_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mKRalpha token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_KRALPHA_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mKRalpha token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_KRALPHA_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mLIQUIDITY/MLiquidityCustomAggregatorFeed.sol b/contracts/products/mLIQUIDITY/MLiquidityCustomAggregatorFeed.sol deleted file mode 100644 index 8215756f..00000000 --- a/contracts/products/mLIQUIDITY/MLiquidityCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MLiquidityMidasAccessControlRoles.sol"; - -/** - * @title MLiquidityCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mLIQUIDITY, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MLiquidityCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MLiquidityMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_LIQUIDITY_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mLIQUIDITY/MLiquidityDataFeed.sol b/contracts/products/mLIQUIDITY/MLiquidityDataFeed.sol deleted file mode 100644 index 07cdce6d..00000000 --- a/contracts/products/mLIQUIDITY/MLiquidityDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MLiquidityMidasAccessControlRoles.sol"; - -/** - * @title MLiquidityDataFeed - * @notice DataFeed for mLIQUIDITY product - * @author RedDuck Software - */ -contract MLiquidityDataFeed is DataFeed, MLiquidityMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_LIQUIDITY_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mLIQUIDITY/MLiquidityDepositVault.sol b/contracts/products/mLIQUIDITY/MLiquidityDepositVault.sol deleted file mode 100644 index a0735cb4..00000000 --- a/contracts/products/mLIQUIDITY/MLiquidityDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MLiquidityMidasAccessControlRoles.sol"; - -/** - * @title MLiquidityDepositVault - * @notice Smart contract that handles mLIQUIDITY minting - * @author RedDuck Software - */ -contract MLiquidityDepositVault is - DepositVault, - MLiquidityMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_LIQUIDITY_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mLIQUIDITY/MLiquidityDepositVaultWithAave.sol b/contracts/products/mLIQUIDITY/MLiquidityDepositVaultWithAave.sol deleted file mode 100644 index a415bb66..00000000 --- a/contracts/products/mLIQUIDITY/MLiquidityDepositVaultWithAave.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVaultWithAave.sol"; -import "./MLiquidityMidasAccessControlRoles.sol"; - -/** - * @title MLiquidityDepositVaultWithAave - * @notice Smart contract that handles mLIQUIDITY minting with Aave V3 auto-invest - * @author RedDuck Software - */ -contract MLiquidityDepositVaultWithAave is - DepositVaultWithAave, - MLiquidityMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_LIQUIDITY_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mLIQUIDITY/MLiquidityDepositVaultWithMorpho.sol b/contracts/products/mLIQUIDITY/MLiquidityDepositVaultWithMorpho.sol deleted file mode 100644 index 4223467b..00000000 --- a/contracts/products/mLIQUIDITY/MLiquidityDepositVaultWithMorpho.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVaultWithMorpho.sol"; -import "./MLiquidityMidasAccessControlRoles.sol"; - -/** - * @title MLiquidityDepositVaultWithMorpho - * @notice Smart contract that handles mLIQUIDITY minting with Morpho auto-invest - * @author RedDuck Software - */ -contract MLiquidityDepositVaultWithMorpho is - DepositVaultWithMorpho, - MLiquidityMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_LIQUIDITY_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mLIQUIDITY/MLiquidityMidasAccessControlRoles.sol b/contracts/products/mLIQUIDITY/MLiquidityMidasAccessControlRoles.sol deleted file mode 100644 index 4b173983..00000000 --- a/contracts/products/mLIQUIDITY/MLiquidityMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MLiquidityMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mLIQUIDITY contracts - * @author RedDuck Software - */ -abstract contract MLiquidityMidasAccessControlRoles { - /** - * @notice actor that can manage MLiquidityDepositVault - */ - bytes32 public constant M_LIQUIDITY_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_LIQUIDITY_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MLiquidityRedemptionVault - */ - bytes32 public constant M_LIQUIDITY_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_LIQUIDITY_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MLiquidityCustomAggregatorFeed and MLiquidityDataFeed - */ - bytes32 public constant M_LIQUIDITY_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_LIQUIDITY_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mLIQUIDITY/MLiquidityRedemptionVault.sol b/contracts/products/mLIQUIDITY/MLiquidityRedemptionVault.sol deleted file mode 100644 index 5544193f..00000000 --- a/contracts/products/mLIQUIDITY/MLiquidityRedemptionVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVault.sol"; -import "./MLiquidityMidasAccessControlRoles.sol"; - -/** - * @title MLiquidityRedemptionVault - * @notice Smart contract that handles mLIQUIDITY redemptions - * @author RedDuck Software - */ -contract MLiquidityRedemptionVault is - RedemptionVault, - MLiquidityMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_LIQUIDITY_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mLIQUIDITY/MLiquidityRedemptionVaultWithAave.sol b/contracts/products/mLIQUIDITY/MLiquidityRedemptionVaultWithAave.sol deleted file mode 100644 index d866dcee..00000000 --- a/contracts/products/mLIQUIDITY/MLiquidityRedemptionVaultWithAave.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithAave.sol"; -import "./MLiquidityMidasAccessControlRoles.sol"; - -/** - * @title MLiquidityRedemptionVaultWithAave - * @notice Smart contract that handles mLIQUIDITY redemptions via Aave V3 - * @author RedDuck Software - */ -contract MLiquidityRedemptionVaultWithAave is - RedemptionVaultWithAave, - MLiquidityMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_LIQUIDITY_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mLIQUIDITY/MLiquidityRedemptionVaultWithMorpho.sol b/contracts/products/mLIQUIDITY/MLiquidityRedemptionVaultWithMorpho.sol deleted file mode 100644 index 14ec2e87..00000000 --- a/contracts/products/mLIQUIDITY/MLiquidityRedemptionVaultWithMorpho.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithMorpho.sol"; -import "./MLiquidityMidasAccessControlRoles.sol"; - -/** - * @title MLiquidityRedemptionVaultWithMorpho - * @notice Smart contract that handles mLIQUIDITY redemptions via Morpho Vault - * @author RedDuck Software - */ -contract MLiquidityRedemptionVaultWithMorpho is - RedemptionVaultWithMorpho, - MLiquidityMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_LIQUIDITY_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mLIQUIDITY/mLIQUIDITY.sol b/contracts/products/mLIQUIDITY/mLIQUIDITY.sol deleted file mode 100644 index 7c71232f..00000000 --- a/contracts/products/mLIQUIDITY/mLIQUIDITY.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title mLIQUIDITY - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mLIQUIDITY is mToken { - /** - * @notice actor that can mint mLIQUIDITY - */ - bytes32 public constant M_LIQUIDITY_MINT_OPERATOR_ROLE = - keccak256("M_LIQUIDITY_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mLIQUIDITY - */ - bytes32 public constant M_LIQUIDITY_BURN_OPERATOR_ROLE = - keccak256("M_LIQUIDITY_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mLIQUIDITY - */ - bytes32 public constant M_LIQUIDITY_PAUSE_OPERATOR_ROLE = - keccak256("M_LIQUIDITY_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas mLIQUIDITY", "mLIQUIDITY"); - } - - /** - * @dev AC role, owner of which can mint mLIQUIDITY token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_LIQUIDITY_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mLIQUIDITY token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_LIQUIDITY_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mLIQUIDITY token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_LIQUIDITY_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mM1USD/MM1UsdCustomAggregatorFeed.sol b/contracts/products/mM1USD/MM1UsdCustomAggregatorFeed.sol deleted file mode 100644 index c733bce1..00000000 --- a/contracts/products/mM1USD/MM1UsdCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MM1UsdMidasAccessControlRoles.sol"; - -/** - * @title MM1UsdCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mM1USD, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MM1UsdCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MM1UsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_M1_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mM1USD/MM1UsdDataFeed.sol b/contracts/products/mM1USD/MM1UsdDataFeed.sol deleted file mode 100644 index 2a37b6df..00000000 --- a/contracts/products/mM1USD/MM1UsdDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MM1UsdMidasAccessControlRoles.sol"; - -/** - * @title MM1UsdDataFeed - * @notice DataFeed for mM1USD product - * @author RedDuck Software - */ -contract MM1UsdDataFeed is DataFeed, MM1UsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_M1_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mM1USD/MM1UsdDepositVault.sol b/contracts/products/mM1USD/MM1UsdDepositVault.sol deleted file mode 100644 index 55bfb3bd..00000000 --- a/contracts/products/mM1USD/MM1UsdDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MM1UsdMidasAccessControlRoles.sol"; - -/** - * @title MM1UsdDepositVault - * @notice Smart contract that handles mM1USD minting - * @author RedDuck Software - */ -contract MM1UsdDepositVault is DepositVault, MM1UsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_M1_USD_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mM1USD/MM1UsdMidasAccessControlRoles.sol b/contracts/products/mM1USD/MM1UsdMidasAccessControlRoles.sol deleted file mode 100644 index 7d894d04..00000000 --- a/contracts/products/mM1USD/MM1UsdMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MM1UsdMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mM1USD contracts - * @author RedDuck Software - */ -abstract contract MM1UsdMidasAccessControlRoles { - /** - * @notice actor that can manage MM1UsdDepositVault - */ - bytes32 public constant M_M1_USD_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_M1_USD_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MM1UsdRedemptionVault - */ - bytes32 public constant M_M1_USD_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_M1_USD_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MM1UsdCustomAggregatorFeed and MM1UsdDataFeed - */ - bytes32 public constant M_M1_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_M1_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mM1USD/MM1UsdRedemptionVaultWithSwapper.sol b/contracts/products/mM1USD/MM1UsdRedemptionVaultWithSwapper.sol deleted file mode 100644 index c7c49a1e..00000000 --- a/contracts/products/mM1USD/MM1UsdRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MM1UsdMidasAccessControlRoles.sol"; - -/** - * @title MM1UsdRedemptionVaultWithSwapper - * @notice Smart contract that handles mM1USD redemptions - * @author RedDuck Software - */ -contract MM1UsdRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MM1UsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_M1_USD_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mM1USD/mM1USD.sol b/contracts/products/mM1USD/mM1USD.sol deleted file mode 100644 index 90aa2b1a..00000000 --- a/contracts/products/mM1USD/mM1USD.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mM1USD - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mM1USD is mToken { - /** - * @notice actor that can mint mM1USD - */ - bytes32 public constant M_M1_USD_MINT_OPERATOR_ROLE = - keccak256("M_M1_USD_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mM1USD - */ - bytes32 public constant M_M1_USD_BURN_OPERATOR_ROLE = - keccak256("M_M1_USD_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mM1USD - */ - bytes32 public constant M_M1_USD_PAUSE_OPERATOR_ROLE = - keccak256("M_M1_USD_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas M1 USD Market Neutral", "mM1-USD"); - } - - /** - * @dev AC role, owner of which can mint mM1USD token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_M1_USD_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mM1USD token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_M1_USD_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mM1USD token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_M1_USD_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mMEV/MMevCustomAggregatorFeed.sol b/contracts/products/mMEV/MMevCustomAggregatorFeed.sol deleted file mode 100644 index 1f7d0817..00000000 --- a/contracts/products/mMEV/MMevCustomAggregatorFeed.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MMevMidasAccessControlRoles.sol"; - -/** - * @title MMevCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mMEV, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MMevCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MMevMidasAccessControlRoles -{ - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_MEV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mMEV/MMevDataFeed.sol b/contracts/products/mMEV/MMevDataFeed.sol deleted file mode 100644 index 6caed85f..00000000 --- a/contracts/products/mMEV/MMevDataFeed.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MMevMidasAccessControlRoles.sol"; - -/** - * @title MMevDataFeed - * @notice DataFeed for mMEV product - * @author RedDuck Software - */ -contract MMevDataFeed is DataFeed, MMevMidasAccessControlRoles { - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_MEV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mMEV/MMevDepositVault.sol b/contracts/products/mMEV/MMevDepositVault.sol deleted file mode 100644 index b80949ed..00000000 --- a/contracts/products/mMEV/MMevDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MMevMidasAccessControlRoles.sol"; - -/** - * @title MMevDepositVault - * @notice Smart contract that handles mMEV minting - * @author RedDuck Software - */ -contract MMevDepositVault is DepositVault, MMevMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_MEV_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mMEV/MMevMidasAccessControlRoles.sol b/contracts/products/mMEV/MMevMidasAccessControlRoles.sol deleted file mode 100644 index 60c18ce6..00000000 --- a/contracts/products/mMEV/MMevMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MMevMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mMEV contracts - * @author RedDuck Software - */ -abstract contract MMevMidasAccessControlRoles { - /** - * @notice actor that can manage MMevDepositVault - */ - bytes32 public constant M_MEV_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_MEV_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MMevRedemptionVault - */ - bytes32 public constant M_MEV_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_MEV_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MMevCustomAggregatorFeed and MMevDataFeed - */ - bytes32 public constant M_MEV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_MEV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mMEV/MMevRedemptionVaultWithSwapper.sol b/contracts/products/mMEV/MMevRedemptionVaultWithSwapper.sol deleted file mode 100644 index 04296506..00000000 --- a/contracts/products/mMEV/MMevRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MMevMidasAccessControlRoles.sol"; - -/** - * @title MMevRedemptionVaultWithSwapper - * @notice Smart contract that handles mMEV redemptions - * @author RedDuck Software - */ -contract MMevRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MMevMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_MEV_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mMEV/mMEV.sol b/contracts/products/mMEV/mMEV.sol deleted file mode 100644 index 7a444a4c..00000000 --- a/contracts/products/mMEV/mMEV.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title mMEV - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mMEV is mToken { - /** - * @notice actor that can mint mMEV - */ - bytes32 public constant M_MEV_MINT_OPERATOR_ROLE = - keccak256("M_MEV_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mMEV - */ - bytes32 public constant M_MEV_BURN_OPERATOR_ROLE = - keccak256("M_MEV_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mMEV - */ - bytes32 public constant M_MEV_PAUSE_OPERATOR_ROLE = - keccak256("M_MEV_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas MEV", "mMEV"); - } - - /** - * @dev AC role, owner of which can mint mMEV token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_MEV_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mMEV token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_MEV_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mMEV token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_MEV_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mMEV/tac/TACmMEV.sol b/contracts/products/mMEV/tac/TACmMEV.sol deleted file mode 100644 index 00ae0718..00000000 --- a/contracts/products/mMEV/tac/TACmMEV.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../../mToken.sol"; - -/** - * @title TACmMEV - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract TACmMEV is mToken { - /** - * @notice actor that can mint TACmMEV - */ - bytes32 public constant TAC_M_MEV_MINT_OPERATOR_ROLE = - keccak256("TAC_M_MEV_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn TACmMEV - */ - bytes32 public constant TAC_M_MEV_BURN_OPERATOR_ROLE = - keccak256("TAC_M_MEV_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause TACmMEV - */ - bytes32 public constant TAC_M_MEV_PAUSE_OPERATOR_ROLE = - keccak256("TAC_M_MEV_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas TACmMEV Token", "TACmMEV"); - } - - /** - * @dev AC role, owner of which can mint TACmMEV token - */ - function _minterRole() internal pure override returns (bytes32) { - return TAC_M_MEV_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn TACmMEV token - */ - function _burnerRole() internal pure override returns (bytes32) { - return TAC_M_MEV_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause TACmMEV token - */ - function _pauserRole() internal pure override returns (bytes32) { - return TAC_M_MEV_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mMEV/tac/TACmMevDepositVault.sol b/contracts/products/mMEV/tac/TACmMevDepositVault.sol deleted file mode 100644 index 026e7577..00000000 --- a/contracts/products/mMEV/tac/TACmMevDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../../DepositVault.sol"; -import "./TACmMevMidasAccessControlRoles.sol"; - -/** - * @title TACmMevDepositVault - * @notice Smart contract that handles TACmMEV minting - * @author RedDuck Software - */ -contract TACmMevDepositVault is DepositVault, TACmMevMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return TAC_M_MEV_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mMEV/tac/TACmMevMidasAccessControlRoles.sol b/contracts/products/mMEV/tac/TACmMevMidasAccessControlRoles.sol deleted file mode 100644 index 57dbc112..00000000 --- a/contracts/products/mMEV/tac/TACmMevMidasAccessControlRoles.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title TACmMevMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for TACmMEV contracts - * @author RedDuck Software - */ -abstract contract TACmMevMidasAccessControlRoles { - /** - * @notice actor that can manage TACmMevDepositVault - */ - bytes32 public constant TAC_M_MEV_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("TAC_M_MEV_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage TACmMevRedemptionVault - */ - bytes32 public constant TAC_M_MEV_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("TAC_M_MEV_REDEMPTION_VAULT_ADMIN_ROLE"); -} diff --git a/contracts/products/mMEV/tac/TACmMevRedemptionVault.sol b/contracts/products/mMEV/tac/TACmMevRedemptionVault.sol deleted file mode 100644 index 57229003..00000000 --- a/contracts/products/mMEV/tac/TACmMevRedemptionVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../../RedemptionVault.sol"; -import "./TACmMevMidasAccessControlRoles.sol"; - -/** - * @title TACmMevRedemptionVault - * @notice Smart contract that handles TACmMEV redemption - * @author RedDuck Software - */ -contract TACmMevRedemptionVault is - RedemptionVault, - TACmMevMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return TAC_M_MEV_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mPortofino/MPortofinoCustomAggregatorFeed.sol b/contracts/products/mPortofino/MPortofinoCustomAggregatorFeed.sol deleted file mode 100644 index 235bfe41..00000000 --- a/contracts/products/mPortofino/MPortofinoCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MPortofinoMidasAccessControlRoles.sol"; - -/** - * @title MPortofinoCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mPortofino, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MPortofinoCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MPortofinoMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_PORTOFINO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mPortofino/MPortofinoDataFeed.sol b/contracts/products/mPortofino/MPortofinoDataFeed.sol deleted file mode 100644 index 3c263dee..00000000 --- a/contracts/products/mPortofino/MPortofinoDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MPortofinoMidasAccessControlRoles.sol"; - -/** - * @title MPortofinoDataFeed - * @notice DataFeed for mPortofino product - * @author RedDuck Software - */ -contract MPortofinoDataFeed is DataFeed, MPortofinoMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_PORTOFINO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mPortofino/MPortofinoDepositVault.sol b/contracts/products/mPortofino/MPortofinoDepositVault.sol deleted file mode 100644 index eca4b865..00000000 --- a/contracts/products/mPortofino/MPortofinoDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MPortofinoMidasAccessControlRoles.sol"; - -/** - * @title MPortofinoDepositVault - * @notice Smart contract that handles mPortofino minting - * @author RedDuck Software - */ -contract MPortofinoDepositVault is - DepositVault, - MPortofinoMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_PORTOFINO_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mPortofino/MPortofinoMidasAccessControlRoles.sol b/contracts/products/mPortofino/MPortofinoMidasAccessControlRoles.sol deleted file mode 100644 index b5094efa..00000000 --- a/contracts/products/mPortofino/MPortofinoMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MPortofinoMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mPortofino contracts - * @author RedDuck Software - */ -abstract contract MPortofinoMidasAccessControlRoles { - /** - * @notice actor that can manage MPortofinoDepositVault - */ - bytes32 public constant M_PORTOFINO_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_PORTOFINO_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MPortofinoRedemptionVault - */ - bytes32 public constant M_PORTOFINO_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_PORTOFINO_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MPortofinoCustomAggregatorFeed and MPortofinoDataFeed - */ - bytes32 public constant M_PORTOFINO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_PORTOFINO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mPortofino/MPortofinoRedemptionVaultWithSwapper.sol b/contracts/products/mPortofino/MPortofinoRedemptionVaultWithSwapper.sol deleted file mode 100644 index 9f78b1e9..00000000 --- a/contracts/products/mPortofino/MPortofinoRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MPortofinoMidasAccessControlRoles.sol"; - -/** - * @title MPortofinoRedemptionVaultWithSwapper - * @notice Smart contract that handles mPortofino redemptions - * @author RedDuck Software - */ -contract MPortofinoRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MPortofinoMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_PORTOFINO_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mPortofino/mPortofino.sol b/contracts/products/mPortofino/mPortofino.sol deleted file mode 100644 index 15e8d831..00000000 --- a/contracts/products/mPortofino/mPortofino.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mPortofino - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mPortofino is mToken { - /** - * @notice actor that can mint mPortofino - */ - bytes32 public constant M_PORTOFINO_MINT_OPERATOR_ROLE = - keccak256("M_PORTOFINO_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mPortofino - */ - bytes32 public constant M_PORTOFINO_BURN_OPERATOR_ROLE = - keccak256("M_PORTOFINO_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mPortofino - */ - bytes32 public constant M_PORTOFINO_PAUSE_OPERATOR_ROLE = - keccak256("M_PORTOFINO_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Portofino", "mPortofino"); - } - - /** - * @dev AC role, owner of which can mint mPortofino token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_PORTOFINO_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mPortofino token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_PORTOFINO_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mPortofino token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_PORTOFINO_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mRE7/MRe7CustomAggregatorFeed.sol b/contracts/products/mRE7/MRe7CustomAggregatorFeed.sol deleted file mode 100644 index 60688f93..00000000 --- a/contracts/products/mRE7/MRe7CustomAggregatorFeed.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MRe7MidasAccessControlRoles.sol"; - -/** - * @title MRe7CustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mRE7, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MRe7CustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MRe7MidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function initialize( - address _accessControl, - int192 _minAnswer, - int192 _maxAnswer, - uint256 _maxAnswerDeviation, - string calldata _description - ) public override { - super.initialize( - _accessControl, - _minAnswer, - _maxAnswer, - _maxAnswerDeviation, - _description - ); - // call v3 to increase contract version to 3 - initializeV3(_maxAnswerDeviation); - } - - /** - * @notice initializes the contract with a new max answer deviation - * @dev increases contract version to 3 (2 was already used) - * @param _newMaxAnswerDeviation new max answer deviation - */ - function initializeV3(uint256 _newMaxAnswerDeviation) - public - reinitializer(3) - { - require( - _newMaxAnswerDeviation <= 100 * (10**decimals()), - "CA: !max deviation" - ); - - maxAnswerDeviation = _newMaxAnswerDeviation; - } - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_RE7_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRE7/MRe7DataFeed.sol b/contracts/products/mRE7/MRe7DataFeed.sol deleted file mode 100644 index 22c303ac..00000000 --- a/contracts/products/mRE7/MRe7DataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MRe7MidasAccessControlRoles.sol"; - -/** - * @title MRe7DataFeed - * @notice DataFeed for mRE7 product - * @author RedDuck Software - */ -contract MRe7DataFeed is DataFeed, MRe7MidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_RE7_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRE7/MRe7DepositVault.sol b/contracts/products/mRE7/MRe7DepositVault.sol deleted file mode 100644 index d7486e22..00000000 --- a/contracts/products/mRE7/MRe7DepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MRe7MidasAccessControlRoles.sol"; - -/** - * @title MRe7DepositVault - * @notice Smart contract that handles mRE7 minting - * @author RedDuck Software - */ -contract MRe7DepositVault is DepositVault, MRe7MidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_RE7_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRE7/MRe7MidasAccessControlRoles.sol b/contracts/products/mRE7/MRe7MidasAccessControlRoles.sol deleted file mode 100644 index 9b643b5c..00000000 --- a/contracts/products/mRE7/MRe7MidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MRe7MidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mRE7 contracts - * @author RedDuck Software - */ -abstract contract MRe7MidasAccessControlRoles { - /** - * @notice actor that can manage MRe7DepositVault - */ - bytes32 public constant M_RE7_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_RE7_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MRe7RedemptionVault - */ - bytes32 public constant M_RE7_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_RE7_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MRe7CustomAggregatorFeed and MRe7DataFeed - */ - bytes32 public constant M_RE7_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_RE7_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mRE7/MRe7RedemptionVaultWithSwapper.sol b/contracts/products/mRE7/MRe7RedemptionVaultWithSwapper.sol deleted file mode 100644 index 803ff46f..00000000 --- a/contracts/products/mRE7/MRe7RedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MRe7MidasAccessControlRoles.sol"; - -/** - * @title MRe7RedemptionVaultWithSwapper - * @notice Smart contract that handles mRE7 redemptions - * @author RedDuck Software - */ -contract MRe7RedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MRe7MidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_RE7_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRE7/mRE7.sol b/contracts/products/mRE7/mRE7.sol deleted file mode 100644 index 890ccb87..00000000 --- a/contracts/products/mRE7/mRE7.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title mRE7 - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mRE7 is mToken { - /** - * @notice actor that can mint mRE7 - */ - bytes32 public constant M_RE7_MINT_OPERATOR_ROLE = - keccak256("M_RE7_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mRE7 - */ - bytes32 public constant M_RE7_BURN_OPERATOR_ROLE = - keccak256("M_RE7_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mRE7 - */ - bytes32 public constant M_RE7_PAUSE_OPERATOR_ROLE = - keccak256("M_RE7_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Re7 Yield", "mRe7YIELD"); - } - - /** - * @dev AC role, owner of which can mint mRE7 token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_RE7_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mRE7 token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_RE7_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mRE7 token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_RE7_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mRE7BTC/MRe7BtcCustomAggregatorFeed.sol b/contracts/products/mRE7BTC/MRe7BtcCustomAggregatorFeed.sol deleted file mode 100644 index b75bb03f..00000000 --- a/contracts/products/mRE7BTC/MRe7BtcCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MRe7BtcMidasAccessControlRoles.sol"; - -/** - * @title MRe7BtcCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mRE7BTC, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MRe7BtcCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MRe7BtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_RE7BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRE7BTC/MRe7BtcDataFeed.sol b/contracts/products/mRE7BTC/MRe7BtcDataFeed.sol deleted file mode 100644 index b703abd0..00000000 --- a/contracts/products/mRE7BTC/MRe7BtcDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MRe7BtcMidasAccessControlRoles.sol"; - -/** - * @title MRe7BtcDataFeed - * @notice DataFeed for mRE7BTC product - * @author RedDuck Software - */ -contract MRe7BtcDataFeed is DataFeed, MRe7BtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_RE7BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRE7BTC/MRe7BtcDepositVault.sol b/contracts/products/mRE7BTC/MRe7BtcDepositVault.sol deleted file mode 100644 index 6913c126..00000000 --- a/contracts/products/mRE7BTC/MRe7BtcDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MRe7BtcMidasAccessControlRoles.sol"; - -/** - * @title MRe7BtcDepositVault - * @notice Smart contract that handles mRE7BTC minting - * @author RedDuck Software - */ -contract MRe7BtcDepositVault is DepositVault, MRe7BtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_RE7BTC_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRE7BTC/MRe7BtcMidasAccessControlRoles.sol b/contracts/products/mRE7BTC/MRe7BtcMidasAccessControlRoles.sol deleted file mode 100644 index a0b269f2..00000000 --- a/contracts/products/mRE7BTC/MRe7BtcMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MRe7BtcMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mRE7BTC contracts - * @author RedDuck Software - */ -abstract contract MRe7BtcMidasAccessControlRoles { - /** - * @notice actor that can manage MRe7BtcDepositVault - */ - bytes32 public constant M_RE7BTC_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_RE7BTC_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MRe7BtcRedemptionVault - */ - bytes32 public constant M_RE7BTC_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_RE7BTC_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MRe7BtcCustomAggregatorFeed and MRe7BtcDataFeed - */ - bytes32 public constant M_RE7BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_RE7BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mRE7BTC/MRe7BtcRedemptionVaultWithSwapper.sol b/contracts/products/mRE7BTC/MRe7BtcRedemptionVaultWithSwapper.sol deleted file mode 100644 index 32b0b9dd..00000000 --- a/contracts/products/mRE7BTC/MRe7BtcRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MRe7BtcMidasAccessControlRoles.sol"; - -/** - * @title MRe7BtcRedemptionVaultWithSwapper - * @notice Smart contract that handles mRE7BTC redemptions - * @author RedDuck Software - */ -contract MRe7BtcRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MRe7BtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_RE7BTC_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRE7BTC/mRE7BTC.sol b/contracts/products/mRE7BTC/mRE7BTC.sol deleted file mode 100644 index 79267022..00000000 --- a/contracts/products/mRE7BTC/mRE7BTC.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mRE7BTC - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mRE7BTC is mToken { - /** - * @notice actor that can mint mRE7BTC - */ - bytes32 public constant M_RE7BTC_MINT_OPERATOR_ROLE = - keccak256("M_RE7BTC_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mRE7BTC - */ - bytes32 public constant M_RE7BTC_BURN_OPERATOR_ROLE = - keccak256("M_RE7BTC_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mRE7BTC - */ - bytes32 public constant M_RE7BTC_PAUSE_OPERATOR_ROLE = - keccak256("M_RE7BTC_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Re7 BTC", "mRe7BTC"); - } - - /** - * @dev AC role, owner of which can mint mRE7BTC token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_RE7BTC_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mRE7BTC token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_RE7BTC_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mRE7BTC token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_RE7BTC_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mRE7SOL/MRe7SolCustomAggregatorFeed.sol b/contracts/products/mRE7SOL/MRe7SolCustomAggregatorFeed.sol deleted file mode 100644 index 3558ea42..00000000 --- a/contracts/products/mRE7SOL/MRe7SolCustomAggregatorFeed.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MRe7SolMidasAccessControlRoles.sol"; - -/** - * @title MRe7SolCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mRE7SOL, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MRe7SolCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MRe7SolMidasAccessControlRoles -{ - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_RE7SOL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRE7SOL/MRe7SolDataFeed.sol b/contracts/products/mRE7SOL/MRe7SolDataFeed.sol deleted file mode 100644 index 4efce428..00000000 --- a/contracts/products/mRE7SOL/MRe7SolDataFeed.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MRe7SolMidasAccessControlRoles.sol"; - -/** - * @title MRe7SolDataFeed - * @notice DataFeed for mRE7SOL product - * @author RedDuck Software - */ -contract MRe7SolDataFeed is DataFeed, MRe7SolMidasAccessControlRoles { - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_RE7SOL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRE7SOL/MRe7SolDepositVault.sol b/contracts/products/mRE7SOL/MRe7SolDepositVault.sol deleted file mode 100644 index 495a9c9c..00000000 --- a/contracts/products/mRE7SOL/MRe7SolDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MRe7SolMidasAccessControlRoles.sol"; - -/** - * @title MRe7SolDepositVault - * @notice Smart contract that handles mRE7SOL minting - * @author RedDuck Software - */ -contract MRe7SolDepositVault is DepositVault, MRe7SolMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_RE7SOL_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRE7SOL/MRe7SolMidasAccessControlRoles.sol b/contracts/products/mRE7SOL/MRe7SolMidasAccessControlRoles.sol deleted file mode 100644 index c173ffa3..00000000 --- a/contracts/products/mRE7SOL/MRe7SolMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MRe7SolMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mRE7SOL contracts - * @author RedDuck Software - */ -abstract contract MRe7SolMidasAccessControlRoles { - /** - * @notice actor that can manage MRe7SolDepositVault - */ - bytes32 public constant M_RE7SOL_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_RE7SOL_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MRe7SolRedemptionVault - */ - bytes32 public constant M_RE7SOL_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_RE7SOL_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MRe7SolCustomAggregatorFeed and MRe7SolDataFeed - */ - bytes32 public constant M_RE7SOL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_RE7SOL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mRE7SOL/MRe7SolRedemptionVault.sol b/contracts/products/mRE7SOL/MRe7SolRedemptionVault.sol deleted file mode 100644 index 1706e790..00000000 --- a/contracts/products/mRE7SOL/MRe7SolRedemptionVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVault.sol"; -import "./MRe7SolMidasAccessControlRoles.sol"; - -/** - * @title MRe7SolRedemptionVault - * @notice Smart contract that handles mRE7SOL redemptions - * @author RedDuck Software - */ -contract MRe7SolRedemptionVault is - RedemptionVault, - MRe7SolMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_RE7SOL_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRE7SOL/mRE7SOL.sol b/contracts/products/mRE7SOL/mRE7SOL.sol deleted file mode 100644 index 04898e9e..00000000 --- a/contracts/products/mRE7SOL/mRE7SOL.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title mRE7SOL - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mRE7SOL is mToken { - /** - * @notice actor that can mint mRE7SOL - */ - bytes32 public constant M_RE7SOL_MINT_OPERATOR_ROLE = - keccak256("M_RE7SOL_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mRE7SOL - */ - bytes32 public constant M_RE7SOL_BURN_OPERATOR_ROLE = - keccak256("M_RE7SOL_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mRE7SOL - */ - bytes32 public constant M_RE7SOL_PAUSE_OPERATOR_ROLE = - keccak256("M_RE7SOL_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Re7SOL", "mRe7SOL"); - } - - /** - * @dev AC role, owner of which can mint mRE7SOL token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_RE7SOL_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mRE7SOL token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_RE7SOL_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mRE7SOL token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_RE7SOL_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mROX/MRoxCustomAggregatorFeed.sol b/contracts/products/mROX/MRoxCustomAggregatorFeed.sol deleted file mode 100644 index a3aa45f8..00000000 --- a/contracts/products/mROX/MRoxCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MRoxMidasAccessControlRoles.sol"; - -/** - * @title MRoxCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mROX, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MRoxCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MRoxMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_ROX_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mROX/MRoxDataFeed.sol b/contracts/products/mROX/MRoxDataFeed.sol deleted file mode 100644 index 3c280591..00000000 --- a/contracts/products/mROX/MRoxDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MRoxMidasAccessControlRoles.sol"; - -/** - * @title MRoxDataFeed - * @notice DataFeed for mROX product - * @author RedDuck Software - */ -contract MRoxDataFeed is DataFeed, MRoxMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_ROX_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mROX/MRoxDepositVault.sol b/contracts/products/mROX/MRoxDepositVault.sol deleted file mode 100644 index a6e53cd7..00000000 --- a/contracts/products/mROX/MRoxDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MRoxMidasAccessControlRoles.sol"; - -/** - * @title MRoxDepositVault - * @notice Smart contract that handles mROX minting - * @author RedDuck Software - */ -contract MRoxDepositVault is DepositVault, MRoxMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_ROX_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mROX/MRoxMidasAccessControlRoles.sol b/contracts/products/mROX/MRoxMidasAccessControlRoles.sol deleted file mode 100644 index 8222c2ad..00000000 --- a/contracts/products/mROX/MRoxMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MRoxMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mROX contracts - * @author RedDuck Software - */ -abstract contract MRoxMidasAccessControlRoles { - /** - * @notice actor that can manage MRoxDepositVault - */ - bytes32 public constant M_ROX_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_ROX_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MRoxRedemptionVault - */ - bytes32 public constant M_ROX_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_ROX_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MRoxCustomAggregatorFeed and MRoxDataFeed - */ - bytes32 public constant M_ROX_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_ROX_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mROX/MRoxRedemptionVaultWithSwapper.sol b/contracts/products/mROX/MRoxRedemptionVaultWithSwapper.sol deleted file mode 100644 index 1b578af8..00000000 --- a/contracts/products/mROX/MRoxRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MRoxMidasAccessControlRoles.sol"; - -/** - * @title MRoxRedemptionVaultWithSwapper - * @notice Smart contract that handles mROX redemptions - * @author RedDuck Software - */ -contract MRoxRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MRoxMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_ROX_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mROX/mROX.sol b/contracts/products/mROX/mROX.sol deleted file mode 100644 index 26a6d71a..00000000 --- a/contracts/products/mROX/mROX.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mROX - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mROX is mToken { - /** - * @notice actor that can mint mROX - */ - bytes32 public constant M_ROX_MINT_OPERATOR_ROLE = - keccak256("M_ROX_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mROX - */ - bytes32 public constant M_ROX_BURN_OPERATOR_ROLE = - keccak256("M_ROX_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mROX - */ - bytes32 public constant M_ROX_PAUSE_OPERATOR_ROLE = - keccak256("M_ROX_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Rockaway Market Neutral", "mROX"); - } - - /** - * @dev AC role, owner of which can mint mROX token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_ROX_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mROX token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_ROX_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mROX token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_ROX_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mRe7ETH/MRe7EthCustomAggregatorFeed.sol b/contracts/products/mRe7ETH/MRe7EthCustomAggregatorFeed.sol deleted file mode 100644 index f36167ea..00000000 --- a/contracts/products/mRe7ETH/MRe7EthCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MRe7EthMidasAccessControlRoles.sol"; - -/** - * @title MRe7EthCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mRe7ETH, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MRe7EthCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MRe7EthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_RE7ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRe7ETH/MRe7EthDataFeed.sol b/contracts/products/mRe7ETH/MRe7EthDataFeed.sol deleted file mode 100644 index 2b445dc9..00000000 --- a/contracts/products/mRe7ETH/MRe7EthDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MRe7EthMidasAccessControlRoles.sol"; - -/** - * @title MRe7EthDataFeed - * @notice DataFeed for mRe7ETH product - * @author RedDuck Software - */ -contract MRe7EthDataFeed is DataFeed, MRe7EthMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_RE7ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRe7ETH/MRe7EthDepositVault.sol b/contracts/products/mRe7ETH/MRe7EthDepositVault.sol deleted file mode 100644 index 75df184a..00000000 --- a/contracts/products/mRe7ETH/MRe7EthDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MRe7EthMidasAccessControlRoles.sol"; - -/** - * @title MRe7EthDepositVault - * @notice Smart contract that handles mRe7ETH minting - * @author RedDuck Software - */ -contract MRe7EthDepositVault is DepositVault, MRe7EthMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_RE7ETH_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRe7ETH/MRe7EthMidasAccessControlRoles.sol b/contracts/products/mRe7ETH/MRe7EthMidasAccessControlRoles.sol deleted file mode 100644 index 8beda66b..00000000 --- a/contracts/products/mRe7ETH/MRe7EthMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MRe7EthMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mRe7ETH contracts - * @author RedDuck Software - */ -abstract contract MRe7EthMidasAccessControlRoles { - /** - * @notice actor that can manage MRe7EthDepositVault - */ - bytes32 public constant M_RE7ETH_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_RE7ETH_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MRe7EthRedemptionVault - */ - bytes32 public constant M_RE7ETH_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_RE7ETH_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MRe7EthCustomAggregatorFeed and MRe7EthDataFeed - */ - bytes32 public constant M_RE7ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_RE7ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mRe7ETH/MRe7EthRedemptionVaultWithSwapper.sol b/contracts/products/mRe7ETH/MRe7EthRedemptionVaultWithSwapper.sol deleted file mode 100644 index 7c517e45..00000000 --- a/contracts/products/mRe7ETH/MRe7EthRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MRe7EthMidasAccessControlRoles.sol"; - -/** - * @title MRe7EthRedemptionVaultWithSwapper - * @notice Smart contract that handles mRe7ETH redemptions - * @author RedDuck Software - */ -contract MRe7EthRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MRe7EthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_RE7ETH_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mRe7ETH/mRe7ETH.sol b/contracts/products/mRe7ETH/mRe7ETH.sol deleted file mode 100644 index a08c04fa..00000000 --- a/contracts/products/mRe7ETH/mRe7ETH.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mRe7ETH - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mRe7ETH is mToken { - /** - * @notice actor that can mint mRe7ETH - */ - bytes32 public constant M_RE7ETH_MINT_OPERATOR_ROLE = - keccak256("M_RE7ETH_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mRe7ETH - */ - bytes32 public constant M_RE7ETH_BURN_OPERATOR_ROLE = - keccak256("M_RE7ETH_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mRe7ETH - */ - bytes32 public constant M_RE7ETH_PAUSE_OPERATOR_ROLE = - keccak256("M_RE7ETH_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Re7 Ethereum", "mRe7ETH"); - } - - /** - * @dev AC role, owner of which can mint mRe7ETH token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_RE7ETH_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mRe7ETH token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_RE7ETH_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mRe7ETH token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_RE7ETH_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mSL/MSLCustomAggregatorFeed.sol b/contracts/products/mSL/MSLCustomAggregatorFeed.sol deleted file mode 100644 index 630ed180..00000000 --- a/contracts/products/mSL/MSLCustomAggregatorFeed.sol +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MSlMidasAccessControlRoles.sol"; - -/** - * @title MSlCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mSL, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MSlCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MSlMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function initialize( - address _accessControl, - int192 _minAnswer, - int192 _maxAnswer, - uint256 _maxAnswerDeviation, - string calldata _description - ) public override { - super.initialize( - _accessControl, - _minAnswer, - _maxAnswer, - _maxAnswerDeviation, - _description - ); - // call v2 to increase contract version to 2 - initializeV2(_maxAnswerDeviation); - } - - /** - * @notice reinitializes the contract with a new max answer deviation - * @param _newMaxAnswerDeviation new max answer deviation - */ - function initializeV2(uint256 _newMaxAnswerDeviation) - public - reinitializer(2) - { - require( - _newMaxAnswerDeviation <= 100 * (10**decimals()), - "CA: !max deviation" - ); - - maxAnswerDeviation = _newMaxAnswerDeviation; - } - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_SL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mSL/MSlDataFeed.sol b/contracts/products/mSL/MSlDataFeed.sol deleted file mode 100644 index ba3b68d4..00000000 --- a/contracts/products/mSL/MSlDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MSlMidasAccessControlRoles.sol"; - -/** - * @title MSlDataFeed - * @notice DataFeed for mSL product - * @author RedDuck Software - */ -contract MSlDataFeed is DataFeed, MSlMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_SL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mSL/MSlDepositVault.sol b/contracts/products/mSL/MSlDepositVault.sol deleted file mode 100644 index 8b20825c..00000000 --- a/contracts/products/mSL/MSlDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MSlMidasAccessControlRoles.sol"; - -/** - * @title MSlDepositVault - * @notice Smart contract that handles mSL minting - * @author RedDuck Software - */ -contract MSlDepositVault is DepositVault, MSlMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_SL_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mSL/MSlMidasAccessControlRoles.sol b/contracts/products/mSL/MSlMidasAccessControlRoles.sol deleted file mode 100644 index 0f29f7f6..00000000 --- a/contracts/products/mSL/MSlMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MSlMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mSL contracts - * @author RedDuck Software - */ -abstract contract MSlMidasAccessControlRoles { - /** - * @notice actor that can manage MSlDepositVault - */ - bytes32 public constant M_SL_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_SL_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MSlRedemptionVault - */ - bytes32 public constant M_SL_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_SL_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MSlCustomAggregatorFeed and MSlDataFeed - */ - bytes32 public constant M_SL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_SL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mSL/MSlRedemptionVaultWithMToken.sol b/contracts/products/mSL/MSlRedemptionVaultWithMToken.sol deleted file mode 100644 index feef42cb..00000000 --- a/contracts/products/mSL/MSlRedemptionVaultWithMToken.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithMToken.sol"; -import "./MSlMidasAccessControlRoles.sol"; - -/** - * @title MSlRedemptionVaultWithMToken - * @notice Smart contract that handles mSL redemptions using mToken - * liquid strategy. Upgrade-compatible replacement for - * MSlRedemptionVaultWithSwapper. - * @author RedDuck Software - */ -contract MSlRedemptionVaultWithMToken is - RedemptionVaultWithMToken, - MSlMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_SL_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mSL/MSlRedemptionVaultWithSwapper.sol b/contracts/products/mSL/MSlRedemptionVaultWithSwapper.sol deleted file mode 100644 index 64da13e2..00000000 --- a/contracts/products/mSL/MSlRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MSlMidasAccessControlRoles.sol"; - -/** - * @title MSlRedemptionVaultWithSwapper - * @notice Smart contract that handles mSL redemptions - * @author RedDuck Software - */ -contract MSlRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MSlMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_SL_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mSL/mSL.sol b/contracts/products/mSL/mSL.sol deleted file mode 100644 index c54f3ef0..00000000 --- a/contracts/products/mSL/mSL.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title mSL - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mSL is mToken { - /** - * @notice actor that can mint mSL - */ - bytes32 public constant M_SL_MINT_OPERATOR_ROLE = - keccak256("M_SL_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mSL - */ - bytes32 public constant M_SL_BURN_OPERATOR_ROLE = - keccak256("M_SL_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mSL - */ - bytes32 public constant M_SL_PAUSE_OPERATOR_ROLE = - keccak256("M_SL_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Staked Liquidity", "mSL"); - } - - /** - * @dev AC role, owner of which can mint mSL token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_SL_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mSL token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_SL_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mSL token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_SL_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mTBILL/MTBillCustomAggregatorFeed.sol b/contracts/products/mTBILL/MTBillCustomAggregatorFeed.sol deleted file mode 100644 index 40741410..00000000 --- a/contracts/products/mTBILL/MTBillCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MTBillMidasAccessControlRoles.sol"; - -/** - * @title MTBillCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mTBILL, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MTBillCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MTBillMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_TBILL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mTBILL/MTBillCustomAggregatorFeedGrowth.sol b/contracts/products/mTBILL/MTBillCustomAggregatorFeedGrowth.sol deleted file mode 100644 index abc7dff5..00000000 --- a/contracts/products/mTBILL/MTBillCustomAggregatorFeedGrowth.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeedGrowth.sol"; -import "./MTBillMidasAccessControlRoles.sol"; - -/** - * @title MTBillCustomAggregatorFeedGrowth - * @notice AggregatorV3 compatible feed for mTBILL, - * where price is submitted manually by feed admins, - * and growth apr applies to the answer. - * @author RedDuck Software - */ -contract MTBillCustomAggregatorFeedGrowth is - CustomAggregatorV3CompatibleFeedGrowth, - MTBillMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeedGrowth - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_TBILL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mTBILL/MTBillDataFeed.sol b/contracts/products/mTBILL/MTBillDataFeed.sol deleted file mode 100644 index 31f6096f..00000000 --- a/contracts/products/mTBILL/MTBillDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MTBillMidasAccessControlRoles.sol"; - -/** - * @title MTBillDataFeed - * @notice DataFeed for mTBILL product - * @author RedDuck Software - */ -contract MTBillDataFeed is DataFeed, MTBillMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_TBILL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mTBILL/MTBillMidasAccessControlRoles.sol b/contracts/products/mTBILL/MTBillMidasAccessControlRoles.sol deleted file mode 100644 index 9c394b0d..00000000 --- a/contracts/products/mTBILL/MTBillMidasAccessControlRoles.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MTBillMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mTBILL contracts - * @author RedDuck Software - */ -abstract contract MTBillMidasAccessControlRoles { - /** - * @notice actor that can manage MTBillCustomAggregatorFeed and MTBillDataFeed - */ - bytes32 public constant M_TBILL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_TBILL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mTBILL/mTBILL.sol b/contracts/products/mTBILL/mTBILL.sol deleted file mode 100644 index ab4231c3..00000000 --- a/contracts/products/mTBILL/mTBILL.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mTBILL - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mTBILL is mToken { - /** - * @notice actor that can mint mTBILL - */ - bytes32 public constant M_TBILL_MINT_OPERATOR_ROLE = - keccak256("M_TBILL_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mTBILL - */ - bytes32 public constant M_TBILL_BURN_OPERATOR_ROLE = - keccak256("M_TBILL_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mTBILL - */ - bytes32 public constant M_TBILL_PAUSE_OPERATOR_ROLE = - keccak256("M_TBILL_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas US Treasury Bill Token", "mTBILL"); - } - - /** - * @dev AC role, owner of which can mint mTBILL token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_TBILL_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mTBILL token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_TBILL_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mTBILL token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_TBILL_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mTEST/MTestCustomAggregatorFeedGrowth.sol b/contracts/products/mTEST/MTestCustomAggregatorFeedGrowth.sol deleted file mode 100644 index f578a6f1..00000000 --- a/contracts/products/mTEST/MTestCustomAggregatorFeedGrowth.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeedGrowth.sol"; -import "./MTestMidasAccessControlRoles.sol"; - -/** - * @title MTestCustomAggregatorFeedGrowth - * @notice AggregatorV3 compatible feed for mTEST, - * where price is submitted manually by feed admins, - * and growth apr applies to the answer. - * @author RedDuck Software - */ -contract MTestCustomAggregatorFeedGrowth is - CustomAggregatorV3CompatibleFeedGrowth, - MTestMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeedGrowth - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_TEST_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mTEST/MTestDataFeed.sol b/contracts/products/mTEST/MTestDataFeed.sol deleted file mode 100644 index 6c2ed01e..00000000 --- a/contracts/products/mTEST/MTestDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MTestMidasAccessControlRoles.sol"; - -/** - * @title MTestDataFeed - * @notice DataFeed for mTEST product - * @author RedDuck Software - */ -contract MTestDataFeed is DataFeed, MTestMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_TEST_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mTEST/MTestDepositVault.sol b/contracts/products/mTEST/MTestDepositVault.sol deleted file mode 100644 index 031567de..00000000 --- a/contracts/products/mTEST/MTestDepositVault.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MTestMidasAccessControlRoles.sol"; - -/** - * @title MTestDepositVault - * @notice Smart contract that handles mTEST minting - * @author RedDuck Software - */ -contract MTestDepositVault is DepositVault, MTestMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_TEST_DEPOSIT_VAULT_ADMIN_ROLE; - } - - /** - * @inheritdoc Greenlistable - */ - function greenlistedRole() public pure override returns (bytes32) { - return M_TEST_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/mTEST/MTestMidasAccessControlRoles.sol b/contracts/products/mTEST/MTestMidasAccessControlRoles.sol deleted file mode 100644 index cfba3aaa..00000000 --- a/contracts/products/mTEST/MTestMidasAccessControlRoles.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MTestMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mTEST contracts - * @author RedDuck Software - */ -abstract contract MTestMidasAccessControlRoles { - /** - * @notice actor that can manage MTestDepositVault - */ - bytes32 public constant M_TEST_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_TEST_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MTestRedemptionVault - */ - bytes32 public constant M_TEST_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_TEST_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MTestCustomAggregatorFeed and MTestDataFeed - */ - bytes32 public constant M_TEST_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_TEST_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); - - /** - * @notice greenlist role for mTEST - */ - bytes32 public constant M_TEST_GREENLISTED_ROLE = - keccak256("M_TEST_GREENLISTED_ROLE"); -} diff --git a/contracts/products/mTEST/MTestRedemptionVaultWithSwapper.sol b/contracts/products/mTEST/MTestRedemptionVaultWithSwapper.sol deleted file mode 100644 index 17982d05..00000000 --- a/contracts/products/mTEST/MTestRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MTestMidasAccessControlRoles.sol"; - -/** - * @title MTestRedemptionVaultWithSwapper - * @notice Smart contract that handles mTEST redemptions - * @author RedDuck Software - */ -contract MTestRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MTestMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_TEST_REDEMPTION_VAULT_ADMIN_ROLE; - } - - /** - * @inheritdoc Greenlistable - */ - function greenlistedRole() public pure override returns (bytes32) { - return M_TEST_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/mTEST/mTEST.sol b/contracts/products/mTEST/mTEST.sol deleted file mode 100644 index 89955248..00000000 --- a/contracts/products/mTEST/mTEST.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mTokenPermissioned.sol"; -import "./MTestMidasAccessControlRoles.sol"; - -/** - * @title mTEST - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mTEST is mTokenPermissioned, MTestMidasAccessControlRoles { - /** - * @notice actor that can mint mTEST - */ - bytes32 public constant M_TEST_MINT_OPERATOR_ROLE = - keccak256("M_TEST_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mTEST - */ - bytes32 public constant M_TEST_BURN_OPERATOR_ROLE = - keccak256("M_TEST_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mTEST - */ - bytes32 public constant M_TEST_PAUSE_OPERATOR_ROLE = - keccak256("M_TEST_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Test", "mTEST"); - } - - /** - * @dev AC role, owner of which can mint mTEST token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_TEST_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mTEST token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_TEST_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mTEST token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_TEST_PAUSE_OPERATOR_ROLE; - } - - /** - * @inheritdoc mTokenPermissioned - */ - function _greenlistedRole() internal pure override returns (bytes32) { - return M_TEST_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/mTU/MTuCustomAggregatorFeed.sol b/contracts/products/mTU/MTuCustomAggregatorFeed.sol deleted file mode 100644 index 4cef4ef0..00000000 --- a/contracts/products/mTU/MTuCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MTuMidasAccessControlRoles.sol"; - -/** - * @title MTuCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mTU, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MTuCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MTuMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_TU_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mTU/MTuDataFeed.sol b/contracts/products/mTU/MTuDataFeed.sol deleted file mode 100644 index 2a7061e8..00000000 --- a/contracts/products/mTU/MTuDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MTuMidasAccessControlRoles.sol"; - -/** - * @title MTuDataFeed - * @notice DataFeed for mTU product - * @author RedDuck Software - */ -contract MTuDataFeed is DataFeed, MTuMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_TU_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mTU/MTuDepositVault.sol b/contracts/products/mTU/MTuDepositVault.sol deleted file mode 100644 index 048ff7cf..00000000 --- a/contracts/products/mTU/MTuDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MTuMidasAccessControlRoles.sol"; - -/** - * @title MTuDepositVault - * @notice Smart contract that handles mTU minting - * @author RedDuck Software - */ -contract MTuDepositVault is DepositVault, MTuMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_TU_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mTU/MTuMidasAccessControlRoles.sol b/contracts/products/mTU/MTuMidasAccessControlRoles.sol deleted file mode 100644 index 758d4304..00000000 --- a/contracts/products/mTU/MTuMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MTuMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mTU contracts - * @author RedDuck Software - */ -abstract contract MTuMidasAccessControlRoles { - /** - * @notice actor that can manage MTuDepositVault - */ - bytes32 public constant M_TU_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_TU_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MTuRedemptionVault - */ - bytes32 public constant M_TU_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_TU_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MTuCustomAggregatorFeed and MTuDataFeed - */ - bytes32 public constant M_TU_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_TU_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mTU/MTuRedemptionVaultWithSwapper.sol b/contracts/products/mTU/MTuRedemptionVaultWithSwapper.sol deleted file mode 100644 index e42dbcf9..00000000 --- a/contracts/products/mTU/MTuRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MTuMidasAccessControlRoles.sol"; - -/** - * @title MTuRedemptionVaultWithSwapper - * @notice Smart contract that handles mTU redemptions - * @author RedDuck Software - */ -contract MTuRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MTuMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_TU_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mTU/mTU.sol b/contracts/products/mTU/mTU.sol deleted file mode 100644 index ac1d81f8..00000000 --- a/contracts/products/mTU/mTU.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mTU - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mTU is mToken { - /** - * @notice actor that can mint mTU - */ - bytes32 public constant M_TU_MINT_OPERATOR_ROLE = - keccak256("M_TU_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mTU - */ - bytes32 public constant M_TU_BURN_OPERATOR_ROLE = - keccak256("M_TU_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mTU - */ - bytes32 public constant M_TU_PAUSE_OPERATOR_ROLE = - keccak256("M_TU_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("X", "mTU"); - } - - /** - * @dev AC role, owner of which can mint mTU token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_TU_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mTU token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_TU_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mTU token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_TU_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mWIN/MWinCustomAggregatorFeed.sol b/contracts/products/mWIN/MWinCustomAggregatorFeed.sol deleted file mode 100644 index 5744ec2b..00000000 --- a/contracts/products/mWIN/MWinCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MWinMidasAccessControlRoles.sol"; - -/** - * @title MWinCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mWIN, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MWinCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MWinMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_WIN_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mWIN/MWinDataFeed.sol b/contracts/products/mWIN/MWinDataFeed.sol deleted file mode 100644 index b7a8dcc8..00000000 --- a/contracts/products/mWIN/MWinDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MWinMidasAccessControlRoles.sol"; - -/** - * @title MWinDataFeed - * @notice DataFeed for mWIN product - * @author RedDuck Software - */ -contract MWinDataFeed is DataFeed, MWinMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_WIN_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mWIN/MWinDepositVault.sol b/contracts/products/mWIN/MWinDepositVault.sol deleted file mode 100644 index ec16f047..00000000 --- a/contracts/products/mWIN/MWinDepositVault.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MWinMidasAccessControlRoles.sol"; - -/** - * @title MWinDepositVault - * @notice Smart contract that handles mWIN minting - * @author RedDuck Software - */ -contract MWinDepositVault is DepositVault, MWinMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_WIN_DEPOSIT_VAULT_ADMIN_ROLE; - } - - /** - * @inheritdoc Greenlistable - */ - function greenlistedRole() public pure override returns (bytes32) { - return M_WIN_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/mWIN/MWinMidasAccessControlRoles.sol b/contracts/products/mWIN/MWinMidasAccessControlRoles.sol deleted file mode 100644 index 15165f03..00000000 --- a/contracts/products/mWIN/MWinMidasAccessControlRoles.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MWinMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mWIN contracts - * @author RedDuck Software - */ -abstract contract MWinMidasAccessControlRoles { - /** - * @notice actor that can manage MWinDepositVault - */ - bytes32 public constant M_WIN_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_WIN_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MWinRedemptionVault - */ - bytes32 public constant M_WIN_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_WIN_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MWinCustomAggregatorFeed and MWinDataFeed - */ - bytes32 public constant M_WIN_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_WIN_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); - - /** - * @notice greenlist role for mWIN - */ - bytes32 public constant M_WIN_GREENLISTED_ROLE = - keccak256("M_WIN_GREENLISTED_ROLE"); -} diff --git a/contracts/products/mWIN/MWinRedemptionVaultWithMToken.sol b/contracts/products/mWIN/MWinRedemptionVaultWithMToken.sol deleted file mode 100644 index 45b5fc54..00000000 --- a/contracts/products/mWIN/MWinRedemptionVaultWithMToken.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithMToken.sol"; -import "./MWinMidasAccessControlRoles.sol"; - -/** - * @title MWinRedemptionVaultWithMToken - * @notice Smart contract that handles mWIN redemptions using mToken - * liquid strategy. Upgrade-compatible replacement for - * MWinRedemptionVaultWithSwapper. - * @author RedDuck Software - */ -contract MWinRedemptionVaultWithMToken is - RedemptionVaultWithMToken, - MWinMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_WIN_REDEMPTION_VAULT_ADMIN_ROLE; - } - - /** - * @inheritdoc Greenlistable - */ - function greenlistedRole() public pure override returns (bytes32) { - return M_WIN_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/mWIN/MWinRedemptionVaultWithSwapper.sol b/contracts/products/mWIN/MWinRedemptionVaultWithSwapper.sol deleted file mode 100644 index 92f99f39..00000000 --- a/contracts/products/mWIN/MWinRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MWinMidasAccessControlRoles.sol"; - -/** - * @title MWinRedemptionVaultWithSwapper - * @notice Smart contract that handles mWIN redemptions - * @author RedDuck Software - */ -contract MWinRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MWinMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_WIN_REDEMPTION_VAULT_ADMIN_ROLE; - } - - /** - * @inheritdoc Greenlistable - */ - function greenlistedRole() public pure override returns (bytes32) { - return M_WIN_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/mWIN/mWIN.sol b/contracts/products/mWIN/mWIN.sol deleted file mode 100644 index c31efe28..00000000 --- a/contracts/products/mWIN/mWIN.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mTokenPermissioned.sol"; -import "./MWinMidasAccessControlRoles.sol"; - -/** - * @title mWIN - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mWIN is mTokenPermissioned, MWinMidasAccessControlRoles { - /** - * @notice actor that can mint mWIN - */ - bytes32 public constant M_WIN_MINT_OPERATOR_ROLE = - keccak256("M_WIN_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mWIN - */ - bytes32 public constant M_WIN_BURN_OPERATOR_ROLE = - keccak256("M_WIN_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mWIN - */ - bytes32 public constant M_WIN_PAUSE_OPERATOR_ROLE = - keccak256("M_WIN_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas Wellington Income Opportunities", "mWIN"); - } - - /** - * @dev AC role, owner of which can mint mWIN token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_WIN_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mWIN token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_WIN_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mWIN token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_WIN_PAUSE_OPERATOR_ROLE; - } - - /** - * @inheritdoc mTokenPermissioned - */ - function _greenlistedRole() internal pure override returns (bytes32) { - return M_WIN_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/mWildUSD/MWildUsdCustomAggregatorFeed.sol b/contracts/products/mWildUSD/MWildUsdCustomAggregatorFeed.sol deleted file mode 100644 index df6f9bfc..00000000 --- a/contracts/products/mWildUSD/MWildUsdCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MWildUsdMidasAccessControlRoles.sol"; - -/** - * @title MWildUsdCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mWildUSD, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MWildUsdCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MWildUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_WILD_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mWildUSD/MWildUsdDataFeed.sol b/contracts/products/mWildUSD/MWildUsdDataFeed.sol deleted file mode 100644 index dad9700e..00000000 --- a/contracts/products/mWildUSD/MWildUsdDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MWildUsdMidasAccessControlRoles.sol"; - -/** - * @title MWildUsdDataFeed - * @notice DataFeed for mWildUSD product - * @author RedDuck Software - */ -contract MWildUsdDataFeed is DataFeed, MWildUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_WILD_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mWildUSD/MWildUsdDepositVault.sol b/contracts/products/mWildUSD/MWildUsdDepositVault.sol deleted file mode 100644 index 8ed32089..00000000 --- a/contracts/products/mWildUSD/MWildUsdDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MWildUsdMidasAccessControlRoles.sol"; - -/** - * @title MWildUsdDepositVault - * @notice Smart contract that handles mWildUSD minting - * @author RedDuck Software - */ -contract MWildUsdDepositVault is DepositVault, MWildUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_WILD_USD_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mWildUSD/MWildUsdMidasAccessControlRoles.sol b/contracts/products/mWildUSD/MWildUsdMidasAccessControlRoles.sol deleted file mode 100644 index 7701e53a..00000000 --- a/contracts/products/mWildUSD/MWildUsdMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MWildUsdMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mWildUSD contracts - * @author RedDuck Software - */ -abstract contract MWildUsdMidasAccessControlRoles { - /** - * @notice actor that can manage MWildUsdDepositVault - */ - bytes32 public constant M_WILD_USD_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_WILD_USD_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MWildUsdRedemptionVault - */ - bytes32 public constant M_WILD_USD_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_WILD_USD_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MWildUsdCustomAggregatorFeed and MWildUsdDataFeed - */ - bytes32 public constant M_WILD_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_WILD_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mWildUSD/MWildUsdRedemptionVaultWithSwapper.sol b/contracts/products/mWildUSD/MWildUsdRedemptionVaultWithSwapper.sol deleted file mode 100644 index 8af4a503..00000000 --- a/contracts/products/mWildUSD/MWildUsdRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MWildUsdMidasAccessControlRoles.sol"; - -/** - * @title MWildUsdRedemptionVaultWithSwapper - * @notice Smart contract that handles mWildUSD redemptions - * @author RedDuck Software - */ -contract MWildUsdRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MWildUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_WILD_USD_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mWildUSD/mWildUSD.sol b/contracts/products/mWildUSD/mWildUSD.sol deleted file mode 100644 index 5289de18..00000000 --- a/contracts/products/mWildUSD/mWildUSD.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mWildUSD - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mWildUSD is mToken { - /** - * @notice actor that can mint mWildUSD - */ - bytes32 public constant M_WILD_USD_MINT_OPERATOR_ROLE = - keccak256("M_WILD_USD_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mWildUSD - */ - bytes32 public constant M_WILD_USD_BURN_OPERATOR_ROLE = - keccak256("M_WILD_USD_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mWildUSD - */ - bytes32 public constant M_WILD_USD_PAUSE_OPERATOR_ROLE = - keccak256("M_WILD_USD_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("mWildUSD", "mWildUSD"); - } - - /** - * @dev AC role, owner of which can mint mWildUSD token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_WILD_USD_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mWildUSD token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_WILD_USD_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mWildUSD token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_WILD_USD_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mXRP/MXrpCustomAggregatorFeed.sol b/contracts/products/mXRP/MXrpCustomAggregatorFeed.sol deleted file mode 100644 index ea0a91db..00000000 --- a/contracts/products/mXRP/MXrpCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MXrpMidasAccessControlRoles.sol"; - -/** - * @title MXrpCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mXRP, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MXrpCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MXrpMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_XRP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mXRP/MXrpDataFeed.sol b/contracts/products/mXRP/MXrpDataFeed.sol deleted file mode 100644 index df280f4f..00000000 --- a/contracts/products/mXRP/MXrpDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MXrpMidasAccessControlRoles.sol"; - -/** - * @title MXrpDataFeed - * @notice DataFeed for mXRP product - * @author RedDuck Software - */ -contract MXrpDataFeed is DataFeed, MXrpMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_XRP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mXRP/MXrpDepositVault.sol b/contracts/products/mXRP/MXrpDepositVault.sol deleted file mode 100644 index c8b01ea8..00000000 --- a/contracts/products/mXRP/MXrpDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MXrpMidasAccessControlRoles.sol"; - -/** - * @title MXrpDepositVault - * @notice Smart contract that handles mXRP minting - * @author RedDuck Software - */ -contract MXrpDepositVault is DepositVault, MXrpMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_XRP_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mXRP/MXrpMidasAccessControlRoles.sol b/contracts/products/mXRP/MXrpMidasAccessControlRoles.sol deleted file mode 100644 index 86774ad0..00000000 --- a/contracts/products/mXRP/MXrpMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MXrpMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mXRP contracts - * @author RedDuck Software - */ -abstract contract MXrpMidasAccessControlRoles { - /** - * @notice actor that can manage MXrpDepositVault - */ - bytes32 public constant M_XRP_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_XRP_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MXrpRedemptionVault - */ - bytes32 public constant M_XRP_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_XRP_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MXrpCustomAggregatorFeed and MXrpDataFeed - */ - bytes32 public constant M_XRP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_XRP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mXRP/MXrpRedemptionVaultWithSwapper.sol b/contracts/products/mXRP/MXrpRedemptionVaultWithSwapper.sol deleted file mode 100644 index 03fe0b28..00000000 --- a/contracts/products/mXRP/MXrpRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MXrpMidasAccessControlRoles.sol"; - -/** - * @title MXrpRedemptionVaultWithSwapper - * @notice Smart contract that handles mXRP redemptions - * @author RedDuck Software - */ -contract MXrpRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MXrpMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_XRP_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mXRP/mXRP.sol b/contracts/products/mXRP/mXRP.sol deleted file mode 100644 index 47af05ea..00000000 --- a/contracts/products/mXRP/mXRP.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title mXRP - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mXRP is mToken { - /** - * @notice actor that can mint mXRP - */ - bytes32 public constant M_XRP_MINT_OPERATOR_ROLE = - keccak256("M_XRP_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mXRP - */ - bytes32 public constant M_XRP_BURN_OPERATOR_ROLE = - keccak256("M_XRP_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mXRP - */ - bytes32 public constant M_XRP_PAUSE_OPERATOR_ROLE = - keccak256("M_XRP_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Midas XRP", "mXRP"); - } - - /** - * @dev AC role, owner of which can mint mXRP token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_XRP_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mXRP token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_XRP_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mXRP token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_XRP_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/mevBTC/MevBtcCustomAggregatorFeed.sol b/contracts/products/mevBTC/MevBtcCustomAggregatorFeed.sol deleted file mode 100644 index 97e36bc1..00000000 --- a/contracts/products/mevBTC/MevBtcCustomAggregatorFeed.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MevBtcMidasAccessControlRoles.sol"; - -/** - * @title MevBtcCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for mevBTC, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MevBtcCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MevBtcMidasAccessControlRoles -{ - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return MEV_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mevBTC/MevBtcDataFeed.sol b/contracts/products/mevBTC/MevBtcDataFeed.sol deleted file mode 100644 index a52b4772..00000000 --- a/contracts/products/mevBTC/MevBtcDataFeed.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MevBtcMidasAccessControlRoles.sol"; - -/** - * @title MevBtcDataFeed - * @notice DataFeed for mevBTC product - * @author RedDuck Software - */ -contract MevBtcDataFeed is DataFeed, MevBtcMidasAccessControlRoles { - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return MEV_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/mevBTC/MevBtcDepositVault.sol b/contracts/products/mevBTC/MevBtcDepositVault.sol deleted file mode 100644 index f495a276..00000000 --- a/contracts/products/mevBTC/MevBtcDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MevBtcMidasAccessControlRoles.sol"; - -/** - * @title MevBtcDepositVault - * @notice Smart contract that handles mevBTC minting - * @author RedDuck Software - */ -contract MevBtcDepositVault is DepositVault, MevBtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return MEV_BTC_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mevBTC/MevBtcMidasAccessControlRoles.sol b/contracts/products/mevBTC/MevBtcMidasAccessControlRoles.sol deleted file mode 100644 index 026b7833..00000000 --- a/contracts/products/mevBTC/MevBtcMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MevBtcMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for mevBTC contracts - * @author RedDuck Software - */ -abstract contract MevBtcMidasAccessControlRoles { - /** - * @notice actor that can manage MevBtcDepositVault - */ - bytes32 public constant MEV_BTC_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("MEV_BTC_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MevBtcRedemptionVault - */ - bytes32 public constant MEV_BTC_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("MEV_BTC_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MevBtcCustomAggregatorFeed and MevBtcDataFeed - */ - bytes32 public constant MEV_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("MEV_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/mevBTC/MevBtcRedemptionVaultWithSwapper.sol b/contracts/products/mevBTC/MevBtcRedemptionVaultWithSwapper.sol deleted file mode 100644 index 4b659bbc..00000000 --- a/contracts/products/mevBTC/MevBtcRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MevBtcMidasAccessControlRoles.sol"; - -/** - * @title MevBtcRedemptionVaultWithSwapper - * @notice Smart contract that handles mevBTC redemptions - * @author RedDuck Software - */ -contract MevBtcRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MevBtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return MEV_BTC_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/mevBTC/mevBTC.sol b/contracts/products/mevBTC/mevBTC.sol deleted file mode 100644 index 18e423fc..00000000 --- a/contracts/products/mevBTC/mevBTC.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title mevBTC - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract mevBTC is mToken { - /** - * @notice actor that can mint mevBTC - */ - bytes32 public constant MEV_BTC_MINT_OPERATOR_ROLE = - keccak256("MEV_BTC_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn mevBTC - */ - bytes32 public constant MEV_BTC_BURN_OPERATOR_ROLE = - keccak256("MEV_BTC_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause mevBTC - */ - bytes32 public constant MEV_BTC_PAUSE_OPERATOR_ROLE = - keccak256("MEV_BTC_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Bitcoin MEV Capital", "mevBTC"); - } - - /** - * @dev AC role, owner of which can mint mevBTC token - */ - function _minterRole() internal pure override returns (bytes32) { - return MEV_BTC_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn mevBTC token - */ - function _burnerRole() internal pure override returns (bytes32) { - return MEV_BTC_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause mevBTC token - */ - function _pauserRole() internal pure override returns (bytes32) { - return MEV_BTC_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/msyrupUSD/MSyrupUsdCustomAggregatorFeed.sol b/contracts/products/msyrupUSD/MSyrupUsdCustomAggregatorFeed.sol deleted file mode 100644 index 773e9ac5..00000000 --- a/contracts/products/msyrupUSD/MSyrupUsdCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MSyrupUsdMidasAccessControlRoles.sol"; - -/** - * @title MSyrupUsdCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for msyrupUSD, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MSyrupUsdCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MSyrupUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_SYRUP_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/msyrupUSD/MSyrupUsdDataFeed.sol b/contracts/products/msyrupUSD/MSyrupUsdDataFeed.sol deleted file mode 100644 index 14cd87ff..00000000 --- a/contracts/products/msyrupUSD/MSyrupUsdDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MSyrupUsdMidasAccessControlRoles.sol"; - -/** - * @title MSyrupUsdDataFeed - * @notice DataFeed for msyrupUSD product - * @author RedDuck Software - */ -contract MSyrupUsdDataFeed is DataFeed, MSyrupUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_SYRUP_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/msyrupUSD/MSyrupUsdDepositVault.sol b/contracts/products/msyrupUSD/MSyrupUsdDepositVault.sol deleted file mode 100644 index 847f29ef..00000000 --- a/contracts/products/msyrupUSD/MSyrupUsdDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MSyrupUsdMidasAccessControlRoles.sol"; - -/** - * @title MSyrupUsdDepositVault - * @notice Smart contract that handles msyrupUSD minting - * @author RedDuck Software - */ -contract MSyrupUsdDepositVault is - DepositVault, - MSyrupUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_SYRUP_USD_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/msyrupUSD/MSyrupUsdMidasAccessControlRoles.sol b/contracts/products/msyrupUSD/MSyrupUsdMidasAccessControlRoles.sol deleted file mode 100644 index 3102c7f2..00000000 --- a/contracts/products/msyrupUSD/MSyrupUsdMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MSyrupUsdMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for msyrupUSD contracts - * @author RedDuck Software - */ -abstract contract MSyrupUsdMidasAccessControlRoles { - /** - * @notice actor that can manage MSyrupUsdDepositVault - */ - bytes32 public constant M_SYRUP_USD_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_SYRUP_USD_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MSyrupUsdRedemptionVault - */ - bytes32 public constant M_SYRUP_USD_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_SYRUP_USD_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MSyrupUsdCustomAggregatorFeed and MSyrupUsdDataFeed - */ - bytes32 public constant M_SYRUP_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_SYRUP_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/msyrupUSD/MSyrupUsdRedemptionVaultWithSwapper.sol b/contracts/products/msyrupUSD/MSyrupUsdRedemptionVaultWithSwapper.sol deleted file mode 100644 index 5e154686..00000000 --- a/contracts/products/msyrupUSD/MSyrupUsdRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MSyrupUsdMidasAccessControlRoles.sol"; - -/** - * @title MSyrupUsdRedemptionVaultWithSwapper - * @notice Smart contract that handles msyrupUSD redemptions - * @author RedDuck Software - */ -contract MSyrupUsdRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MSyrupUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_SYRUP_USD_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/msyrupUSD/msyrupUSD.sol b/contracts/products/msyrupUSD/msyrupUSD.sol deleted file mode 100644 index 7868e407..00000000 --- a/contracts/products/msyrupUSD/msyrupUSD.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title msyrupUSD - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract msyrupUSD is mToken { - /** - * @notice actor that can mint msyrupUSD - */ - bytes32 public constant M_SYRUP_USD_MINT_OPERATOR_ROLE = - keccak256("M_SYRUP_USD_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn msyrupUSD - */ - bytes32 public constant M_SYRUP_USD_BURN_OPERATOR_ROLE = - keccak256("M_SYRUP_USD_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause msyrupUSD - */ - bytes32 public constant M_SYRUP_USD_PAUSE_OPERATOR_ROLE = - keccak256("M_SYRUP_USD_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("syrupUSDC supercharged", "msyrupUSD"); - } - - /** - * @dev AC role, owner of which can mint msyrupUSD token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_SYRUP_USD_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn msyrupUSD token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_SYRUP_USD_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause msyrupUSD token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_SYRUP_USD_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/msyrupUSDp/MSyrupUsdpCustomAggregatorFeed.sol b/contracts/products/msyrupUSDp/MSyrupUsdpCustomAggregatorFeed.sol deleted file mode 100644 index f02a90cb..00000000 --- a/contracts/products/msyrupUSDp/MSyrupUsdpCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./MSyrupUsdpMidasAccessControlRoles.sol"; - -/** - * @title MSyrupUsdpCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for msyrupUSDp, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract MSyrupUsdpCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - MSyrupUsdpMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_SYRUP_USDP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/msyrupUSDp/MSyrupUsdpDataFeed.sol b/contracts/products/msyrupUSDp/MSyrupUsdpDataFeed.sol deleted file mode 100644 index e751883d..00000000 --- a/contracts/products/msyrupUSDp/MSyrupUsdpDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./MSyrupUsdpMidasAccessControlRoles.sol"; - -/** - * @title MSyrupUsdpDataFeed - * @notice DataFeed for msyrupUSDp product - * @author RedDuck Software - */ -contract MSyrupUsdpDataFeed is DataFeed, MSyrupUsdpMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return M_SYRUP_USDP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/msyrupUSDp/MSyrupUsdpDepositVault.sol b/contracts/products/msyrupUSDp/MSyrupUsdpDepositVault.sol deleted file mode 100644 index 11224905..00000000 --- a/contracts/products/msyrupUSDp/MSyrupUsdpDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./MSyrupUsdpMidasAccessControlRoles.sol"; - -/** - * @title MSyrupUsdpDepositVault - * @notice Smart contract that handles msyrupUSDp minting - * @author RedDuck Software - */ -contract MSyrupUsdpDepositVault is - DepositVault, - MSyrupUsdpMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_SYRUP_USDP_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/msyrupUSDp/MSyrupUsdpMidasAccessControlRoles.sol b/contracts/products/msyrupUSDp/MSyrupUsdpMidasAccessControlRoles.sol deleted file mode 100644 index 9b2bc7d6..00000000 --- a/contracts/products/msyrupUSDp/MSyrupUsdpMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title MSyrupUsdpMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for msyrupUSDp contracts - * @author RedDuck Software - */ -abstract contract MSyrupUsdpMidasAccessControlRoles { - /** - * @notice actor that can manage MSyrupUsdpDepositVault - */ - bytes32 public constant M_SYRUP_USDP_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("M_SYRUP_USDP_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MSyrupUsdpRedemptionVault - */ - bytes32 public constant M_SYRUP_USDP_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("M_SYRUP_USDP_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage MSyrupUsdpCustomAggregatorFeed and MSyrupUsdpDataFeed - */ - bytes32 public constant M_SYRUP_USDP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("M_SYRUP_USDP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/msyrupUSDp/MSyrupUsdpRedemptionVaultWithSwapper.sol b/contracts/products/msyrupUSDp/MSyrupUsdpRedemptionVaultWithSwapper.sol deleted file mode 100644 index b9b14ce1..00000000 --- a/contracts/products/msyrupUSDp/MSyrupUsdpRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./MSyrupUsdpMidasAccessControlRoles.sol"; - -/** - * @title MSyrupUsdpRedemptionVaultWithSwapper - * @notice Smart contract that handles msyrupUSDp redemptions - * @author RedDuck Software - */ -contract MSyrupUsdpRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - MSyrupUsdpMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return M_SYRUP_USDP_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/msyrupUSDp/msyrupUSDp.sol b/contracts/products/msyrupUSDp/msyrupUSDp.sol deleted file mode 100644 index f2d4f831..00000000 --- a/contracts/products/msyrupUSDp/msyrupUSDp.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title msyrupUSDp - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract msyrupUSDp is mToken { - /** - * @notice actor that can mint msyrupUSDp - */ - bytes32 public constant M_SYRUP_USDP_MINT_OPERATOR_ROLE = - keccak256("M_SYRUP_USDP_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn msyrupUSDp - */ - bytes32 public constant M_SYRUP_USDP_BURN_OPERATOR_ROLE = - keccak256("M_SYRUP_USDP_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause msyrupUSDp - */ - bytes32 public constant M_SYRUP_USDP_PAUSE_OPERATOR_ROLE = - keccak256("M_SYRUP_USDP_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Plasma syrupUSD Pre-deposit Midas Vault", "msyrupUSDp"); - } - - /** - * @dev AC role, owner of which can mint msyrupUSDp token - */ - function _minterRole() internal pure override returns (bytes32) { - return M_SYRUP_USDP_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn msyrupUSDp token - */ - function _burnerRole() internal pure override returns (bytes32) { - return M_SYRUP_USDP_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause msyrupUSDp token - */ - function _pauserRole() internal pure override returns (bytes32) { - return M_SYRUP_USDP_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/obeatUSD/ObeatUsdCustomAggregatorFeed.sol b/contracts/products/obeatUSD/ObeatUsdCustomAggregatorFeed.sol deleted file mode 100644 index ad286a4e..00000000 --- a/contracts/products/obeatUSD/ObeatUsdCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./ObeatUsdMidasAccessControlRoles.sol"; - -/** - * @title ObeatUsdCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for obeatUSD, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract ObeatUsdCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - ObeatUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return OBEAT_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/obeatUSD/ObeatUsdDataFeed.sol b/contracts/products/obeatUSD/ObeatUsdDataFeed.sol deleted file mode 100644 index 5e79eef5..00000000 --- a/contracts/products/obeatUSD/ObeatUsdDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./ObeatUsdMidasAccessControlRoles.sol"; - -/** - * @title ObeatUsdDataFeed - * @notice DataFeed for obeatUSD product - * @author RedDuck Software - */ -contract ObeatUsdDataFeed is DataFeed, ObeatUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return OBEAT_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/obeatUSD/ObeatUsdDepositVault.sol b/contracts/products/obeatUSD/ObeatUsdDepositVault.sol deleted file mode 100644 index 7ee00a56..00000000 --- a/contracts/products/obeatUSD/ObeatUsdDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./ObeatUsdMidasAccessControlRoles.sol"; - -/** - * @title ObeatUsdDepositVault - * @notice Smart contract that handles obeatUSD minting - * @author RedDuck Software - */ -contract ObeatUsdDepositVault is DepositVault, ObeatUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return OBEAT_USD_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/obeatUSD/ObeatUsdMidasAccessControlRoles.sol b/contracts/products/obeatUSD/ObeatUsdMidasAccessControlRoles.sol deleted file mode 100644 index e05ada06..00000000 --- a/contracts/products/obeatUSD/ObeatUsdMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title ObeatUsdMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for obeatUSD contracts - * @author RedDuck Software - */ -abstract contract ObeatUsdMidasAccessControlRoles { - /** - * @notice actor that can manage ObeatUsdDepositVault - */ - bytes32 public constant OBEAT_USD_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("OBEAT_USD_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage ObeatUsdRedemptionVault - */ - bytes32 public constant OBEAT_USD_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("OBEAT_USD_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage ObeatUsdCustomAggregatorFeed and ObeatUsdDataFeed - */ - bytes32 public constant OBEAT_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("OBEAT_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/obeatUSD/ObeatUsdRedemptionVaultWithSwapper.sol b/contracts/products/obeatUSD/ObeatUsdRedemptionVaultWithSwapper.sol deleted file mode 100644 index b3a470f9..00000000 --- a/contracts/products/obeatUSD/ObeatUsdRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./ObeatUsdMidasAccessControlRoles.sol"; - -/** - * @title ObeatUsdRedemptionVaultWithSwapper - * @notice Smart contract that handles obeatUSD redemptions - * @author RedDuck Software - */ -contract ObeatUsdRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - ObeatUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return OBEAT_USD_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/obeatUSD/obeatUSD.sol b/contracts/products/obeatUSD/obeatUSD.sol deleted file mode 100644 index 32147418..00000000 --- a/contracts/products/obeatUSD/obeatUSD.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title obeatUSD - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract obeatUSD is mToken { - /** - * @notice actor that can mint obeatUSD - */ - bytes32 public constant OBEAT_USD_MINT_OPERATOR_ROLE = - keccak256("OBEAT_USD_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn obeatUSD - */ - bytes32 public constant OBEAT_USD_BURN_OPERATOR_ROLE = - keccak256("OBEAT_USD_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause obeatUSD - */ - bytes32 public constant OBEAT_USD_PAUSE_OPERATOR_ROLE = - keccak256("OBEAT_USD_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("OmniBeat USD", "obeatUSD"); - } - - /** - * @dev AC role, owner of which can mint obeatUSD token - */ - function _minterRole() internal pure override returns (bytes32) { - return OBEAT_USD_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn obeatUSD token - */ - function _burnerRole() internal pure override returns (bytes32) { - return OBEAT_USD_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause obeatUSD token - */ - function _pauserRole() internal pure override returns (bytes32) { - return OBEAT_USD_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/plUSD/PlUsdCustomAggregatorFeed.sol b/contracts/products/plUSD/PlUsdCustomAggregatorFeed.sol deleted file mode 100644 index df02dbfc..00000000 --- a/contracts/products/plUSD/PlUsdCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./PlUsdMidasAccessControlRoles.sol"; - -/** - * @title PlUsdCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for plUSD, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract PlUsdCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - PlUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return PL_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/plUSD/PlUsdDataFeed.sol b/contracts/products/plUSD/PlUsdDataFeed.sol deleted file mode 100644 index 49a6fcc1..00000000 --- a/contracts/products/plUSD/PlUsdDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./PlUsdMidasAccessControlRoles.sol"; - -/** - * @title PlUsdDataFeed - * @notice DataFeed for plUSD product - * @author RedDuck Software - */ -contract PlUsdDataFeed is DataFeed, PlUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return PL_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/plUSD/PlUsdDepositVault.sol b/contracts/products/plUSD/PlUsdDepositVault.sol deleted file mode 100644 index 4a2d1a47..00000000 --- a/contracts/products/plUSD/PlUsdDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./PlUsdMidasAccessControlRoles.sol"; - -/** - * @title PlUsdDepositVault - * @notice Smart contract that handles plUSD minting - * @author RedDuck Software - */ -contract PlUsdDepositVault is DepositVault, PlUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return PL_USD_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/plUSD/PlUsdMidasAccessControlRoles.sol b/contracts/products/plUSD/PlUsdMidasAccessControlRoles.sol deleted file mode 100644 index 714c50a9..00000000 --- a/contracts/products/plUSD/PlUsdMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title PlUsdMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for plUSD contracts - * @author RedDuck Software - */ -abstract contract PlUsdMidasAccessControlRoles { - /** - * @notice actor that can manage PlUsdDepositVault - */ - bytes32 public constant PL_USD_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("PL_USD_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage PlUsdRedemptionVault - */ - bytes32 public constant PL_USD_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("PL_USD_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage PlUsdCustomAggregatorFeed and PlUsdDataFeed - */ - bytes32 public constant PL_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("PL_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/plUSD/PlUsdRedemptionVaultWithSwapper.sol b/contracts/products/plUSD/PlUsdRedemptionVaultWithSwapper.sol deleted file mode 100644 index 62a07b27..00000000 --- a/contracts/products/plUSD/PlUsdRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./PlUsdMidasAccessControlRoles.sol"; - -/** - * @title PlUsdRedemptionVaultWithSwapper - * @notice Smart contract that handles plUSD redemptions - * @author RedDuck Software - */ -contract PlUsdRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - PlUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return PL_USD_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/plUSD/plUSD.sol b/contracts/products/plUSD/plUSD.sol deleted file mode 100644 index e11750d1..00000000 --- a/contracts/products/plUSD/plUSD.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title plUSD - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract plUSD is mToken { - /** - * @notice actor that can mint plUSD - */ - bytes32 public constant PL_USD_MINT_OPERATOR_ROLE = - keccak256("PL_USD_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn plUSD - */ - bytes32 public constant PL_USD_BURN_OPERATOR_ROLE = - keccak256("PL_USD_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause plUSD - */ - bytes32 public constant PL_USD_PAUSE_OPERATOR_ROLE = - keccak256("PL_USD_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Plasma USD", "plUSD"); - } - - /** - * @dev AC role, owner of which can mint plUSD token - */ - function _minterRole() internal pure override returns (bytes32) { - return PL_USD_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn plUSD token - */ - function _burnerRole() internal pure override returns (bytes32) { - return PL_USD_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause plUSD token - */ - function _pauserRole() internal pure override returns (bytes32) { - return PL_USD_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/qHVNUSD/QHVNUsdCustomAggregatorFeed.sol b/contracts/products/qHVNUSD/QHVNUsdCustomAggregatorFeed.sol deleted file mode 100644 index bf0ac580..00000000 --- a/contracts/products/qHVNUSD/QHVNUsdCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./QHVNUsdMidasAccessControlRoles.sol"; - -/** - * @title QHVNUsdCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for qHVNUSD, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract QHVNUsdCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - QHVNUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return Q_HVN_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/qHVNUSD/QHVNUsdDataFeed.sol b/contracts/products/qHVNUSD/QHVNUsdDataFeed.sol deleted file mode 100644 index c6753244..00000000 --- a/contracts/products/qHVNUSD/QHVNUsdDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./QHVNUsdMidasAccessControlRoles.sol"; - -/** - * @title QHVNUsdDataFeed - * @notice DataFeed for qHVNUSD product - * @author RedDuck Software - */ -contract QHVNUsdDataFeed is DataFeed, QHVNUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return Q_HVN_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/qHVNUSD/QHVNUsdDepositVault.sol b/contracts/products/qHVNUSD/QHVNUsdDepositVault.sol deleted file mode 100644 index cdc3fc9c..00000000 --- a/contracts/products/qHVNUSD/QHVNUsdDepositVault.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./QHVNUsdMidasAccessControlRoles.sol"; - -/** - * @title QHVNUsdDepositVault - * @notice Smart contract that handles qHVNUSD minting - * @author RedDuck Software - */ -contract QHVNUsdDepositVault is DepositVault, QHVNUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return Q_HVN_USD_DEPOSIT_VAULT_ADMIN_ROLE; - } - - /** - * @inheritdoc Greenlistable - */ - function greenlistedRole() public pure override returns (bytes32) { - return Q_HVN_USD_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/qHVNUSD/QHVNUsdMidasAccessControlRoles.sol b/contracts/products/qHVNUSD/QHVNUsdMidasAccessControlRoles.sol deleted file mode 100644 index 00229ccd..00000000 --- a/contracts/products/qHVNUSD/QHVNUsdMidasAccessControlRoles.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title QHVNUsdMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for qHVNUSD contracts - * @author RedDuck Software - */ -abstract contract QHVNUsdMidasAccessControlRoles { - /** - * @notice actor that can manage QHVNUsdDepositVault - */ - bytes32 public constant Q_HVN_USD_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("Q_HVN_USD_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage QHVNUsdRedemptionVault - */ - bytes32 public constant Q_HVN_USD_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("Q_HVN_USD_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage QHVNUsdCustomAggregatorFeed and QHVNUsdDataFeed - */ - bytes32 public constant Q_HVN_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("Q_HVN_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); - - /** - * @notice greenlist role for qHVNUSD - */ - bytes32 public constant Q_HVN_USD_GREENLISTED_ROLE = - keccak256("Q_HVN_USD_GREENLISTED_ROLE"); -} diff --git a/contracts/products/qHVNUSD/QHVNUsdRedemptionVaultWithSwapper.sol b/contracts/products/qHVNUSD/QHVNUsdRedemptionVaultWithSwapper.sol deleted file mode 100644 index 7efc066a..00000000 --- a/contracts/products/qHVNUSD/QHVNUsdRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./QHVNUsdMidasAccessControlRoles.sol"; - -/** - * @title QHVNUsdRedemptionVaultWithSwapper - * @notice Smart contract that handles qHVNUSD redemptions - * @author RedDuck Software - */ -contract QHVNUsdRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - QHVNUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return Q_HVN_USD_REDEMPTION_VAULT_ADMIN_ROLE; - } - - /** - * @inheritdoc Greenlistable - */ - function greenlistedRole() public pure override returns (bytes32) { - return Q_HVN_USD_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/qHVNUSD/qHVNUSD.sol b/contracts/products/qHVNUSD/qHVNUSD.sol deleted file mode 100644 index 2afba0b7..00000000 --- a/contracts/products/qHVNUSD/qHVNUSD.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mTokenPermissioned.sol"; -import "./QHVNUsdMidasAccessControlRoles.sol"; - -/** - * @title qHVNUSD - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract qHVNUSD is mTokenPermissioned, QHVNUsdMidasAccessControlRoles { - /** - * @notice actor that can mint qHVNUSD - */ - bytes32 public constant Q_HVN_USD_MINT_OPERATOR_ROLE = - keccak256("Q_HVN_USD_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn qHVNUSD - */ - bytes32 public constant Q_HVN_USD_BURN_OPERATOR_ROLE = - keccak256("Q_HVN_USD_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause qHVNUSD - */ - bytes32 public constant Q_HVN_USD_PAUSE_OPERATOR_ROLE = - keccak256("Q_HVN_USD_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Qapture Safe Haven", "QHVN-USD"); - } - - /** - * @dev AC role, owner of which can mint qHVNUSD token - */ - function _minterRole() internal pure override returns (bytes32) { - return Q_HVN_USD_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn qHVNUSD token - */ - function _burnerRole() internal pure override returns (bytes32) { - return Q_HVN_USD_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause qHVNUSD token - */ - function _pauserRole() internal pure override returns (bytes32) { - return Q_HVN_USD_PAUSE_OPERATOR_ROLE; - } - - /** - * @inheritdoc mTokenPermissioned - */ - function _greenlistedRole() internal pure override returns (bytes32) { - return Q_HVN_USD_GREENLISTED_ROLE; - } -} diff --git a/contracts/products/sGold/SGoldCustomAggregatorFeed.sol b/contracts/products/sGold/SGoldCustomAggregatorFeed.sol deleted file mode 100644 index 4326157d..00000000 --- a/contracts/products/sGold/SGoldCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./SGoldMidasAccessControlRoles.sol"; - -/** - * @title SGoldCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for sGold, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract SGoldCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - SGoldMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return S_GOLD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/sGold/SGoldDataFeed.sol b/contracts/products/sGold/SGoldDataFeed.sol deleted file mode 100644 index 89ca2adb..00000000 --- a/contracts/products/sGold/SGoldDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./SGoldMidasAccessControlRoles.sol"; - -/** - * @title SGoldDataFeed - * @notice DataFeed for sGold product - * @author RedDuck Software - */ -contract SGoldDataFeed is DataFeed, SGoldMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return S_GOLD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/sGold/SGoldDepositVault.sol b/contracts/products/sGold/SGoldDepositVault.sol deleted file mode 100644 index 9ebe8b58..00000000 --- a/contracts/products/sGold/SGoldDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./SGoldMidasAccessControlRoles.sol"; - -/** - * @title SGoldDepositVault - * @notice Smart contract that handles sGold minting - * @author RedDuck Software - */ -contract SGoldDepositVault is DepositVault, SGoldMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return S_GOLD_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/sGold/SGoldMidasAccessControlRoles.sol b/contracts/products/sGold/SGoldMidasAccessControlRoles.sol deleted file mode 100644 index 6df363a0..00000000 --- a/contracts/products/sGold/SGoldMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title SGoldMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for sGold contracts - * @author RedDuck Software - */ -abstract contract SGoldMidasAccessControlRoles { - /** - * @notice actor that can manage SGoldDepositVault - */ - bytes32 public constant S_GOLD_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("S_GOLD_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage SGoldRedemptionVault - */ - bytes32 public constant S_GOLD_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("S_GOLD_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage SGoldCustomAggregatorFeed and SGoldDataFeed - */ - bytes32 public constant S_GOLD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("S_GOLD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/sGold/SGoldRedemptionVaultWithSwapper.sol b/contracts/products/sGold/SGoldRedemptionVaultWithSwapper.sol deleted file mode 100644 index 9468bff2..00000000 --- a/contracts/products/sGold/SGoldRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./SGoldMidasAccessControlRoles.sol"; - -/** - * @title SGoldRedemptionVaultWithSwapper - * @notice Smart contract that handles sGold redemptions - * @author RedDuck Software - */ -contract SGoldRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - SGoldMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return S_GOLD_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/sGold/sGold.sol b/contracts/products/sGold/sGold.sol deleted file mode 100644 index 3bcefc35..00000000 --- a/contracts/products/sGold/sGold.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title sGold - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract sGold is mToken { - /** - * @notice actor that can mint sGold - */ - bytes32 public constant S_GOLD_MINT_OPERATOR_ROLE = - keccak256("S_GOLD_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn sGold - */ - bytes32 public constant S_GOLD_BURN_OPERATOR_ROLE = - keccak256("S_GOLD_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause sGold - */ - bytes32 public constant S_GOLD_PAUSE_OPERATOR_ROLE = - keccak256("S_GOLD_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Suissequant Gold Yield", "sGold"); - } - - /** - * @dev AC role, owner of which can mint sGold token - */ - function _minterRole() internal pure override returns (bytes32) { - return S_GOLD_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn sGold token - */ - function _burnerRole() internal pure override returns (bytes32) { - return S_GOLD_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause sGold token - */ - function _pauserRole() internal pure override returns (bytes32) { - return S_GOLD_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/sLINJ/SLInjCustomAggregatorFeed.sol b/contracts/products/sLINJ/SLInjCustomAggregatorFeed.sol deleted file mode 100644 index 97ccf1f7..00000000 --- a/contracts/products/sLINJ/SLInjCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./SLInjMidasAccessControlRoles.sol"; - -/** - * @title SLInjCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for sLINJ, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract SLInjCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - SLInjMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return SL_INJ_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/sLINJ/SLInjDataFeed.sol b/contracts/products/sLINJ/SLInjDataFeed.sol deleted file mode 100644 index e2111018..00000000 --- a/contracts/products/sLINJ/SLInjDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./SLInjMidasAccessControlRoles.sol"; - -/** - * @title SLInjDataFeed - * @notice DataFeed for sLINJ product - * @author RedDuck Software - */ -contract SLInjDataFeed is DataFeed, SLInjMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return SL_INJ_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/sLINJ/SLInjDepositVault.sol b/contracts/products/sLINJ/SLInjDepositVault.sol deleted file mode 100644 index 0a59144b..00000000 --- a/contracts/products/sLINJ/SLInjDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./SLInjMidasAccessControlRoles.sol"; - -/** - * @title SLInjDepositVault - * @notice Smart contract that handles sLINJ minting - * @author RedDuck Software - */ -contract SLInjDepositVault is DepositVault, SLInjMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return SL_INJ_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/sLINJ/SLInjMidasAccessControlRoles.sol b/contracts/products/sLINJ/SLInjMidasAccessControlRoles.sol deleted file mode 100644 index e1ac857b..00000000 --- a/contracts/products/sLINJ/SLInjMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title SLInjMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for sLINJ contracts - * @author RedDuck Software - */ -abstract contract SLInjMidasAccessControlRoles { - /** - * @notice actor that can manage SLInjDepositVault - */ - bytes32 public constant SL_INJ_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("SL_INJ_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage SLInjRedemptionVault - */ - bytes32 public constant SL_INJ_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("SL_INJ_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage SLInjCustomAggregatorFeed and SLInjDataFeed - */ - bytes32 public constant SL_INJ_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("SL_INJ_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/sLINJ/SLInjRedemptionVaultWithSwapper.sol b/contracts/products/sLINJ/SLInjRedemptionVaultWithSwapper.sol deleted file mode 100644 index 631f57c5..00000000 --- a/contracts/products/sLINJ/SLInjRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./SLInjMidasAccessControlRoles.sol"; - -/** - * @title SLInjRedemptionVaultWithSwapper - * @notice Smart contract that handles sLINJ redemptions - * @author RedDuck Software - */ -contract SLInjRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - SLInjMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return SL_INJ_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/sLINJ/sLINJ.sol b/contracts/products/sLINJ/sLINJ.sol deleted file mode 100644 index 2e18767a..00000000 --- a/contracts/products/sLINJ/sLINJ.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title sLINJ - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract sLINJ is mToken { - /** - * @notice actor that can mint sLINJ - */ - bytes32 public constant SL_INJ_MINT_OPERATOR_ROLE = - keccak256("SL_INJ_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn sLINJ - */ - bytes32 public constant SL_INJ_BURN_OPERATOR_ROLE = - keccak256("SL_INJ_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause sLINJ - */ - bytes32 public constant SL_INJ_PAUSE_OPERATOR_ROLE = - keccak256("SL_INJ_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("INJ Loop Stake Vault", "sLINJ"); - } - - /** - * @dev AC role, owner of which can mint sLINJ token - */ - function _minterRole() internal pure override returns (bytes32) { - return SL_INJ_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn sLINJ token - */ - function _burnerRole() internal pure override returns (bytes32) { - return SL_INJ_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause sLINJ token - */ - function _pauserRole() internal pure override returns (bytes32) { - return SL_INJ_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/splUSD/SplUsdCustomAggregatorFeed.sol b/contracts/products/splUSD/SplUsdCustomAggregatorFeed.sol deleted file mode 100644 index a68f0bc0..00000000 --- a/contracts/products/splUSD/SplUsdCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./SplUsdMidasAccessControlRoles.sol"; - -/** - * @title SplUsdCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for splUSD, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract SplUsdCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - SplUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return SPL_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/splUSD/SplUsdDataFeed.sol b/contracts/products/splUSD/SplUsdDataFeed.sol deleted file mode 100644 index f8920ebc..00000000 --- a/contracts/products/splUSD/SplUsdDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./SplUsdMidasAccessControlRoles.sol"; - -/** - * @title SplUsdDataFeed - * @notice DataFeed for splUSD product - * @author RedDuck Software - */ -contract SplUsdDataFeed is DataFeed, SplUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return SPL_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/splUSD/SplUsdDepositVault.sol b/contracts/products/splUSD/SplUsdDepositVault.sol deleted file mode 100644 index 24f3b764..00000000 --- a/contracts/products/splUSD/SplUsdDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./SplUsdMidasAccessControlRoles.sol"; - -/** - * @title SplUsdDepositVault - * @notice Smart contract that handles splUSD minting - * @author RedDuck Software - */ -contract SplUsdDepositVault is DepositVault, SplUsdMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return SPL_USD_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/splUSD/SplUsdMidasAccessControlRoles.sol b/contracts/products/splUSD/SplUsdMidasAccessControlRoles.sol deleted file mode 100644 index a06865b4..00000000 --- a/contracts/products/splUSD/SplUsdMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title SplUsdMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for splUSD contracts - * @author RedDuck Software - */ -abstract contract SplUsdMidasAccessControlRoles { - /** - * @notice actor that can manage SplUsdDepositVault - */ - bytes32 public constant SPL_USD_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("SPL_USD_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage SplUsdRedemptionVault - */ - bytes32 public constant SPL_USD_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("SPL_USD_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage SplUsdCustomAggregatorFeed and SplUsdDataFeed - */ - bytes32 public constant SPL_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("SPL_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/splUSD/SplUsdRedemptionVaultWithSwapper.sol b/contracts/products/splUSD/SplUsdRedemptionVaultWithSwapper.sol deleted file mode 100644 index 4b7ed3ac..00000000 --- a/contracts/products/splUSD/SplUsdRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./SplUsdMidasAccessControlRoles.sol"; - -/** - * @title SplUsdRedemptionVaultWithSwapper - * @notice Smart contract that handles splUSD redemptions - * @author RedDuck Software - */ -contract SplUsdRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - SplUsdMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return SPL_USD_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/splUSD/splUSD.sol b/contracts/products/splUSD/splUSD.sol deleted file mode 100644 index 7e0c22c3..00000000 --- a/contracts/products/splUSD/splUSD.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title splUSD - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract splUSD is mToken { - /** - * @notice actor that can mint splUSD - */ - bytes32 public constant SPL_USD_MINT_OPERATOR_ROLE = - keccak256("SPL_USD_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn splUSD - */ - bytes32 public constant SPL_USD_BURN_OPERATOR_ROLE = - keccak256("SPL_USD_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause splUSD - */ - bytes32 public constant SPL_USD_PAUSE_OPERATOR_ROLE = - keccak256("SPL_USD_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Staked Plasma USD", "splUSD"); - } - - /** - * @dev AC role, owner of which can mint splUSD token - */ - function _minterRole() internal pure override returns (bytes32) { - return SPL_USD_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn splUSD token - */ - function _burnerRole() internal pure override returns (bytes32) { - return SPL_USD_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause splUSD token - */ - function _pauserRole() internal pure override returns (bytes32) { - return SPL_USD_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeCustomAggregatorFeed.sol b/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeCustomAggregatorFeed.sol deleted file mode 100644 index 3dd7eb35..00000000 --- a/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./StockMarketTRBasisTradeMidasAccessControlRoles.sol"; - -/** - * @title StockMarketTRBasisTradeCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for stockMarketTRBasisTrade, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract StockMarketTRBasisTradeCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - StockMarketTRBasisTradeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return STOCK_MARKET_TR_BASIS_TRADE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeDataFeed.sol b/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeDataFeed.sol deleted file mode 100644 index 2bd592e5..00000000 --- a/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeDataFeed.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./StockMarketTRBasisTradeMidasAccessControlRoles.sol"; - -/** - * @title StockMarketTRBasisTradeDataFeed - * @notice DataFeed for stockMarketTRBasisTrade product - * @author RedDuck Software - */ -contract StockMarketTRBasisTradeDataFeed is - DataFeed, - StockMarketTRBasisTradeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return STOCK_MARKET_TR_BASIS_TRADE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeDepositVault.sol b/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeDepositVault.sol deleted file mode 100644 index 68626fab..00000000 --- a/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./StockMarketTRBasisTradeMidasAccessControlRoles.sol"; - -/** - * @title StockMarketTRBasisTradeDepositVault - * @notice Smart contract that handles stockMarketTRBasisTrade minting - * @author RedDuck Software - */ -contract StockMarketTRBasisTradeDepositVault is - DepositVault, - StockMarketTRBasisTradeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return STOCK_MARKET_TR_BASIS_TRADE_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeMidasAccessControlRoles.sol b/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeMidasAccessControlRoles.sol deleted file mode 100644 index bacf33cb..00000000 --- a/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeMidasAccessControlRoles.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title StockMarketTRBasisTradeMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for stockMarketTRBasisTrade contracts - * @author RedDuck Software - */ -abstract contract StockMarketTRBasisTradeMidasAccessControlRoles { - /** - * @notice actor that can manage StockMarketTRBasisTradeDepositVault - */ - bytes32 - public constant STOCK_MARKET_TR_BASIS_TRADE_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("STOCK_MARKET_TR_BASIS_TRADE_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage StockMarketTRBasisTradeRedemptionVault - */ - bytes32 - public constant STOCK_MARKET_TR_BASIS_TRADE_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("STOCK_MARKET_TR_BASIS_TRADE_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage StockMarketTRBasisTradeCustomAggregatorFeed and StockMarketTRBasisTradeDataFeed - */ - bytes32 - public constant STOCK_MARKET_TR_BASIS_TRADE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256( - "STOCK_MARKET_TR_BASIS_TRADE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE" - ); -} diff --git a/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeRedemptionVaultWithSwapper.sol b/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeRedemptionVaultWithSwapper.sol deleted file mode 100644 index 95e965a3..00000000 --- a/contracts/products/stockMarketTRBasisTrade/StockMarketTRBasisTradeRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./StockMarketTRBasisTradeMidasAccessControlRoles.sol"; - -/** - * @title StockMarketTRBasisTradeRedemptionVaultWithSwapper - * @notice Smart contract that handles stockMarketTRBasisTrade redemptions - * @author RedDuck Software - */ -contract StockMarketTRBasisTradeRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - StockMarketTRBasisTradeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return STOCK_MARKET_TR_BASIS_TRADE_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/stockMarketTRBasisTrade/stockMarketTRBasisTrade.sol b/contracts/products/stockMarketTRBasisTrade/stockMarketTRBasisTrade.sol deleted file mode 100644 index 70751f90..00000000 --- a/contracts/products/stockMarketTRBasisTrade/stockMarketTRBasisTrade.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title stockMarketTRBasisTrade - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract stockMarketTRBasisTrade is mToken { - /** - * @notice actor that can mint stockMarketTRBasisTrade - */ - bytes32 public constant STOCK_MARKET_TR_BASIS_TRADE_MINT_OPERATOR_ROLE = - keccak256("STOCK_MARKET_TR_BASIS_TRADE_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn stockMarketTRBasisTrade - */ - bytes32 public constant STOCK_MARKET_TR_BASIS_TRADE_BURN_OPERATOR_ROLE = - keccak256("STOCK_MARKET_TR_BASIS_TRADE_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause stockMarketTRBasisTrade - */ - bytes32 public constant STOCK_MARKET_TR_BASIS_TRADE_PAUSE_OPERATOR_ROLE = - keccak256("STOCK_MARKET_TR_BASIS_TRADE_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ( - "Morini StockMarketTRBasisTrade Vault", - "StockMarketTRBasisTrade" - ); - } - - /** - * @dev AC role, owner of which can mint stockMarketTRBasisTrade token - */ - function _minterRole() internal pure override returns (bytes32) { - return STOCK_MARKET_TR_BASIS_TRADE_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn stockMarketTRBasisTrade token - */ - function _burnerRole() internal pure override returns (bytes32) { - return STOCK_MARKET_TR_BASIS_TRADE_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause stockMarketTRBasisTrade token - */ - function _pauserRole() internal pure override returns (bytes32) { - return STOCK_MARKET_TR_BASIS_TRADE_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/tBTC/TBtcCustomAggregatorFeed.sol b/contracts/products/tBTC/TBtcCustomAggregatorFeed.sol deleted file mode 100644 index 7030e53a..00000000 --- a/contracts/products/tBTC/TBtcCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./TBtcMidasAccessControlRoles.sol"; - -/** - * @title TBtcCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for tBTC, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract TBtcCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - TBtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return T_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/tBTC/TBtcDataFeed.sol b/contracts/products/tBTC/TBtcDataFeed.sol deleted file mode 100644 index 8659ea20..00000000 --- a/contracts/products/tBTC/TBtcDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./TBtcMidasAccessControlRoles.sol"; - -/** - * @title TBtcDataFeed - * @notice DataFeed for tBTC product - * @author RedDuck Software - */ -contract TBtcDataFeed is DataFeed, TBtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return T_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/tBTC/TBtcDepositVault.sol b/contracts/products/tBTC/TBtcDepositVault.sol deleted file mode 100644 index cfd1180e..00000000 --- a/contracts/products/tBTC/TBtcDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./TBtcMidasAccessControlRoles.sol"; - -/** - * @title TBtcDepositVault - * @notice Smart contract that handles tBTC minting - * @author RedDuck Software - */ -contract TBtcDepositVault is DepositVault, TBtcMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return T_BTC_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/tBTC/TBtcMidasAccessControlRoles.sol b/contracts/products/tBTC/TBtcMidasAccessControlRoles.sol deleted file mode 100644 index b108394b..00000000 --- a/contracts/products/tBTC/TBtcMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title TBtcMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for tBTC contracts - * @author RedDuck Software - */ -abstract contract TBtcMidasAccessControlRoles { - /** - * @notice actor that can manage TBtcDepositVault - */ - bytes32 public constant T_BTC_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("T_BTC_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage TBtcRedemptionVault - */ - bytes32 public constant T_BTC_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("T_BTC_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage TBtcCustomAggregatorFeed and TBtcDataFeed - */ - bytes32 public constant T_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("T_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/tBTC/TBtcRedemptionVaultWithSwapper.sol b/contracts/products/tBTC/TBtcRedemptionVaultWithSwapper.sol deleted file mode 100644 index 682950fb..00000000 --- a/contracts/products/tBTC/TBtcRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./TBtcMidasAccessControlRoles.sol"; - -/** - * @title TBtcRedemptionVaultWithSwapper - * @notice Smart contract that handles tBTC redemptions - * @author RedDuck Software - */ -contract TBtcRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - TBtcMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return T_BTC_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/tBTC/tBTC.sol b/contracts/products/tBTC/tBTC.sol deleted file mode 100644 index 06d7d437..00000000 --- a/contracts/products/tBTC/tBTC.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title tBTC - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract tBTC is mToken { - /** - * @notice actor that can mint tBTC - */ - bytes32 public constant T_BTC_MINT_OPERATOR_ROLE = - keccak256("T_BTC_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn tBTC - */ - bytes32 public constant T_BTC_BURN_OPERATOR_ROLE = - keccak256("T_BTC_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause tBTC - */ - bytes32 public constant T_BTC_PAUSE_OPERATOR_ROLE = - keccak256("T_BTC_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Terminal WBTC", "tBTC"); - } - - /** - * @dev AC role, owner of which can mint tBTC token - */ - function _minterRole() internal pure override returns (bytes32) { - return T_BTC_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn tBTC token - */ - function _burnerRole() internal pure override returns (bytes32) { - return T_BTC_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause tBTC token - */ - function _pauserRole() internal pure override returns (bytes32) { - return T_BTC_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/tETH/TEthCustomAggregatorFeed.sol b/contracts/products/tETH/TEthCustomAggregatorFeed.sol deleted file mode 100644 index 6b166603..00000000 --- a/contracts/products/tETH/TEthCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./TEthMidasAccessControlRoles.sol"; - -/** - * @title TEthCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for tETH, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract TEthCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - TEthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return T_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/tETH/TEthDataFeed.sol b/contracts/products/tETH/TEthDataFeed.sol deleted file mode 100644 index b954f09a..00000000 --- a/contracts/products/tETH/TEthDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./TEthMidasAccessControlRoles.sol"; - -/** - * @title TEthDataFeed - * @notice DataFeed for tETH product - * @author RedDuck Software - */ -contract TEthDataFeed is DataFeed, TEthMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return T_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/tETH/TEthDepositVault.sol b/contracts/products/tETH/TEthDepositVault.sol deleted file mode 100644 index 18b2a9ed..00000000 --- a/contracts/products/tETH/TEthDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./TEthMidasAccessControlRoles.sol"; - -/** - * @title TEthDepositVault - * @notice Smart contract that handles tETH minting - * @author RedDuck Software - */ -contract TEthDepositVault is DepositVault, TEthMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return T_ETH_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/tETH/TEthMidasAccessControlRoles.sol b/contracts/products/tETH/TEthMidasAccessControlRoles.sol deleted file mode 100644 index 6ca27744..00000000 --- a/contracts/products/tETH/TEthMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title TEthMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for tETH contracts - * @author RedDuck Software - */ -abstract contract TEthMidasAccessControlRoles { - /** - * @notice actor that can manage TEthDepositVault - */ - bytes32 public constant T_ETH_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("T_ETH_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage TEthRedemptionVault - */ - bytes32 public constant T_ETH_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("T_ETH_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage TEthCustomAggregatorFeed and TEthDataFeed - */ - bytes32 public constant T_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("T_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/tETH/TEthRedemptionVaultWithSwapper.sol b/contracts/products/tETH/TEthRedemptionVaultWithSwapper.sol deleted file mode 100644 index 65f6f942..00000000 --- a/contracts/products/tETH/TEthRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./TEthMidasAccessControlRoles.sol"; - -/** - * @title TEthRedemptionVaultWithSwapper - * @notice Smart contract that handles tETH redemptions - * @author RedDuck Software - */ -contract TEthRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - TEthMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return T_ETH_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/tETH/tETH.sol b/contracts/products/tETH/tETH.sol deleted file mode 100644 index 49c0814a..00000000 --- a/contracts/products/tETH/tETH.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title tETH - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract tETH is mToken { - /** - * @notice actor that can mint tETH - */ - bytes32 public constant T_ETH_MINT_OPERATOR_ROLE = - keccak256("T_ETH_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn tETH - */ - bytes32 public constant T_ETH_BURN_OPERATOR_ROLE = - keccak256("T_ETH_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause tETH - */ - bytes32 public constant T_ETH_PAUSE_OPERATOR_ROLE = - keccak256("T_ETH_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Terminal WETH", "tETH"); - } - - /** - * @dev AC role, owner of which can mint tETH token - */ - function _minterRole() internal pure override returns (bytes32) { - return T_ETH_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn tETH token - */ - function _burnerRole() internal pure override returns (bytes32) { - return T_ETH_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause tETH token - */ - function _pauserRole() internal pure override returns (bytes32) { - return T_ETH_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/tUSDe/TUsdeCustomAggregatorFeed.sol b/contracts/products/tUSDe/TUsdeCustomAggregatorFeed.sol deleted file mode 100644 index 9028869b..00000000 --- a/contracts/products/tUSDe/TUsdeCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./TUsdeMidasAccessControlRoles.sol"; - -/** - * @title TUsdeCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for tUSDe, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract TUsdeCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - TUsdeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return T_USDE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/tUSDe/TUsdeDataFeed.sol b/contracts/products/tUSDe/TUsdeDataFeed.sol deleted file mode 100644 index e302a64d..00000000 --- a/contracts/products/tUSDe/TUsdeDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./TUsdeMidasAccessControlRoles.sol"; - -/** - * @title TUsdeDataFeed - * @notice DataFeed for tUSDe product - * @author RedDuck Software - */ -contract TUsdeDataFeed is DataFeed, TUsdeMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return T_USDE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/tUSDe/TUsdeDepositVault.sol b/contracts/products/tUSDe/TUsdeDepositVault.sol deleted file mode 100644 index 0829b461..00000000 --- a/contracts/products/tUSDe/TUsdeDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./TUsdeMidasAccessControlRoles.sol"; - -/** - * @title TUsdeDepositVault - * @notice Smart contract that handles tUSDe minting - * @author RedDuck Software - */ -contract TUsdeDepositVault is DepositVault, TUsdeMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return T_USDE_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/tUSDe/TUsdeMidasAccessControlRoles.sol b/contracts/products/tUSDe/TUsdeMidasAccessControlRoles.sol deleted file mode 100644 index b6feb409..00000000 --- a/contracts/products/tUSDe/TUsdeMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title TUsdeMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for tUSDe contracts - * @author RedDuck Software - */ -abstract contract TUsdeMidasAccessControlRoles { - /** - * @notice actor that can manage TUsdeDepositVault - */ - bytes32 public constant T_USDE_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("T_USDE_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage TUsdeRedemptionVault - */ - bytes32 public constant T_USDE_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("T_USDE_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage TUsdeCustomAggregatorFeed and TUsdeDataFeed - */ - bytes32 public constant T_USDE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("T_USDE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/tUSDe/TUsdeRedemptionVaultWithSwapper.sol b/contracts/products/tUSDe/TUsdeRedemptionVaultWithSwapper.sol deleted file mode 100644 index b3a9c7c7..00000000 --- a/contracts/products/tUSDe/TUsdeRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./TUsdeMidasAccessControlRoles.sol"; - -/** - * @title TUsdeRedemptionVaultWithSwapper - * @notice Smart contract that handles tUSDe redemptions - * @author RedDuck Software - */ -contract TUsdeRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - TUsdeMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return T_USDE_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/tUSDe/tUSDe.sol b/contracts/products/tUSDe/tUSDe.sol deleted file mode 100644 index 1e08d6b2..00000000 --- a/contracts/products/tUSDe/tUSDe.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; -import "../../mToken.sol"; - -/** - * @title tUSDe - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract tUSDe is mToken { - /** - * @notice actor that can mint tUSDe - */ - bytes32 public constant T_USDE_MINT_OPERATOR_ROLE = - keccak256("T_USDE_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn tUSDe - */ - bytes32 public constant T_USDE_BURN_OPERATOR_ROLE = - keccak256("T_USDE_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause tUSDe - */ - bytes32 public constant T_USDE_PAUSE_OPERATOR_ROLE = - keccak256("T_USDE_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Terminal USDe", "tUSDe"); - } - - /** - * @dev AC role, owner of which can mint tUSDe token - */ - function _minterRole() internal pure override returns (bytes32) { - return T_USDE_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn tUSDe token - */ - function _burnerRole() internal pure override returns (bytes32) { - return T_USDE_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause tUSDe token - */ - function _pauserRole() internal pure override returns (bytes32) { - return T_USDE_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/tacTON/TacTonCustomAggregatorFeed.sol b/contracts/products/tacTON/TacTonCustomAggregatorFeed.sol deleted file mode 100644 index e61434bf..00000000 --- a/contracts/products/tacTON/TacTonCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./TacTonMidasAccessControlRoles.sol"; - -/** - * @title TacTonCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for tacTON, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract TacTonCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - TacTonMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return TAC_TON_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/tacTON/TacTonDataFeed.sol b/contracts/products/tacTON/TacTonDataFeed.sol deleted file mode 100644 index 6a6090b9..00000000 --- a/contracts/products/tacTON/TacTonDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./TacTonMidasAccessControlRoles.sol"; - -/** - * @title TacTonDataFeed - * @notice DataFeed for tacTON product - * @author RedDuck Software - */ -contract TacTonDataFeed is DataFeed, TacTonMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return TAC_TON_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/tacTON/TacTonDepositVault.sol b/contracts/products/tacTON/TacTonDepositVault.sol deleted file mode 100644 index 6e377917..00000000 --- a/contracts/products/tacTON/TacTonDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./TacTonMidasAccessControlRoles.sol"; - -/** - * @title TacTonDepositVault - * @notice Smart contract that handles tacTON minting - * @author RedDuck Software - */ -contract TacTonDepositVault is DepositVault, TacTonMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return TAC_TON_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/tacTON/TacTonMidasAccessControlRoles.sol b/contracts/products/tacTON/TacTonMidasAccessControlRoles.sol deleted file mode 100644 index d4073452..00000000 --- a/contracts/products/tacTON/TacTonMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title TacTonMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for tacTON contracts - * @author RedDuck Software - */ -abstract contract TacTonMidasAccessControlRoles { - /** - * @notice actor that can manage TacTonDepositVault - */ - bytes32 public constant TAC_TON_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("TAC_TON_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage TacTonRedemptionVault - */ - bytes32 public constant TAC_TON_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("TAC_TON_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage TacTonCustomAggregatorFeed and TacTonDataFeed - */ - bytes32 public constant TAC_TON_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("TAC_TON_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/tacTON/TacTonRedemptionVaultWithSwapper.sol b/contracts/products/tacTON/TacTonRedemptionVaultWithSwapper.sol deleted file mode 100644 index 5c4f512d..00000000 --- a/contracts/products/tacTON/TacTonRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./TacTonMidasAccessControlRoles.sol"; - -/** - * @title TacTonRedemptionVaultWithSwapper - * @notice Smart contract that handles tacTON redemptions - * @author RedDuck Software - */ -contract TacTonRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - TacTonMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return TAC_TON_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/tacTON/tacTON.sol b/contracts/products/tacTON/tacTON.sol deleted file mode 100644 index c2c2daec..00000000 --- a/contracts/products/tacTON/tacTON.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title tacTON - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract tacTON is mToken { - /** - * @notice actor that can mint tacTON - */ - bytes32 public constant TAC_TON_MINT_OPERATOR_ROLE = - keccak256("TAC_TON_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn tacTON - */ - bytes32 public constant TAC_TON_BURN_OPERATOR_ROLE = - keccak256("TAC_TON_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause tacTON - */ - bytes32 public constant TAC_TON_PAUSE_OPERATOR_ROLE = - keccak256("TAC_TON_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("tacTON", "tacTON"); - } - - /** - * @dev AC role, owner of which can mint tacTON token - */ - function _minterRole() internal pure override returns (bytes32) { - return TAC_TON_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn tacTON token - */ - function _burnerRole() internal pure override returns (bytes32) { - return TAC_TON_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause tacTON token - */ - function _pauserRole() internal pure override returns (bytes32) { - return TAC_TON_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/turtlePST/TurtlePstCustomAggregatorFeed.sol b/contracts/products/turtlePST/TurtlePstCustomAggregatorFeed.sol deleted file mode 100644 index 4761d652..00000000 --- a/contracts/products/turtlePST/TurtlePstCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./TurtlePstMidasAccessControlRoles.sol"; - -/** - * @title TurtlePstCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for turtlePST, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract TurtlePstCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - TurtlePstMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return TURTLE_PST_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/turtlePST/TurtlePstDataFeed.sol b/contracts/products/turtlePST/TurtlePstDataFeed.sol deleted file mode 100644 index 7155a0da..00000000 --- a/contracts/products/turtlePST/TurtlePstDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./TurtlePstMidasAccessControlRoles.sol"; - -/** - * @title TurtlePstDataFeed - * @notice DataFeed for turtlePST product - * @author RedDuck Software - */ -contract TurtlePstDataFeed is DataFeed, TurtlePstMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return TURTLE_PST_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/turtlePST/TurtlePstDepositVault.sol b/contracts/products/turtlePST/TurtlePstDepositVault.sol deleted file mode 100644 index 95b67345..00000000 --- a/contracts/products/turtlePST/TurtlePstDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./TurtlePstMidasAccessControlRoles.sol"; - -/** - * @title TurtlePstDepositVault - * @notice Smart contract that handles turtlePST minting - * @author RedDuck Software - */ -contract TurtlePstDepositVault is - DepositVault, - TurtlePstMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return TURTLE_PST_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/turtlePST/TurtlePstMidasAccessControlRoles.sol b/contracts/products/turtlePST/TurtlePstMidasAccessControlRoles.sol deleted file mode 100644 index 1db8f66a..00000000 --- a/contracts/products/turtlePST/TurtlePstMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title TurtlePstMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for turtlePST contracts - * @author RedDuck Software - */ -abstract contract TurtlePstMidasAccessControlRoles { - /** - * @notice actor that can manage TurtlePstDepositVault - */ - bytes32 public constant TURTLE_PST_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("TURTLE_PST_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage TurtlePstRedemptionVault - */ - bytes32 public constant TURTLE_PST_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("TURTLE_PST_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage TurtlePstCustomAggregatorFeed and TurtlePstDataFeed - */ - bytes32 public constant TURTLE_PST_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("TURTLE_PST_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/turtlePST/TurtlePstRedemptionVaultWithSwapper.sol b/contracts/products/turtlePST/TurtlePstRedemptionVaultWithSwapper.sol deleted file mode 100644 index 3469976d..00000000 --- a/contracts/products/turtlePST/TurtlePstRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./TurtlePstMidasAccessControlRoles.sol"; - -/** - * @title TurtlePstRedemptionVaultWithSwapper - * @notice Smart contract that handles turtlePST redemptions - * @author RedDuck Software - */ -contract TurtlePstRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - TurtlePstMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return TURTLE_PST_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/turtlePST/turtlePST.sol b/contracts/products/turtlePST/turtlePST.sol deleted file mode 100644 index 5d1e6752..00000000 --- a/contracts/products/turtlePST/turtlePST.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title turtlePST - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract turtlePST is mToken { - /** - * @notice actor that can mint turtlePST - */ - bytes32 public constant TURTLE_PST_MINT_OPERATOR_ROLE = - keccak256("TURTLE_PST_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn turtlePST - */ - bytes32 public constant TURTLE_PST_BURN_OPERATOR_ROLE = - keccak256("TURTLE_PST_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause turtlePST - */ - bytes32 public constant TURTLE_PST_PAUSE_OPERATOR_ROLE = - keccak256("TURTLE_PST_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Turtle Huma PST Vault", "turtlePST"); - } - - /** - * @dev AC role, owner of which can mint turtlePST token - */ - function _minterRole() internal pure override returns (bytes32) { - return TURTLE_PST_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn turtlePST token - */ - function _burnerRole() internal pure override returns (bytes32) { - return TURTLE_PST_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause turtlePST token - */ - function _pauserRole() internal pure override returns (bytes32) { - return TURTLE_PST_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/wNLP/WNlpCustomAggregatorFeed.sol b/contracts/products/wNLP/WNlpCustomAggregatorFeed.sol deleted file mode 100644 index 95243a6a..00000000 --- a/contracts/products/wNLP/WNlpCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./WNlpMidasAccessControlRoles.sol"; - -/** - * @title WNlpCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for wNLP, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract WNlpCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - WNlpMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return W_NLP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/wNLP/WNlpDataFeed.sol b/contracts/products/wNLP/WNlpDataFeed.sol deleted file mode 100644 index 9c96d64c..00000000 --- a/contracts/products/wNLP/WNlpDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./WNlpMidasAccessControlRoles.sol"; - -/** - * @title WNlpDataFeed - * @notice DataFeed for wNLP product - * @author RedDuck Software - */ -contract WNlpDataFeed is DataFeed, WNlpMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return W_NLP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/wNLP/WNlpDepositVault.sol b/contracts/products/wNLP/WNlpDepositVault.sol deleted file mode 100644 index 43f4dae5..00000000 --- a/contracts/products/wNLP/WNlpDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./WNlpMidasAccessControlRoles.sol"; - -/** - * @title WNlpDepositVault - * @notice Smart contract that handles wNLP minting - * @author RedDuck Software - */ -contract WNlpDepositVault is DepositVault, WNlpMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return W_NLP_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/wNLP/WNlpMidasAccessControlRoles.sol b/contracts/products/wNLP/WNlpMidasAccessControlRoles.sol deleted file mode 100644 index ea05d1f3..00000000 --- a/contracts/products/wNLP/WNlpMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title WNlpMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for wNLP contracts - * @author RedDuck Software - */ -abstract contract WNlpMidasAccessControlRoles { - /** - * @notice actor that can manage WNlpDepositVault - */ - bytes32 public constant W_NLP_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("W_NLP_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage WNlpRedemptionVault - */ - bytes32 public constant W_NLP_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("W_NLP_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage WNlpCustomAggregatorFeed and WNlpDataFeed - */ - bytes32 public constant W_NLP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("W_NLP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/wNLP/WNlpRedemptionVaultWithSwapper.sol b/contracts/products/wNLP/WNlpRedemptionVaultWithSwapper.sol deleted file mode 100644 index b9a3c7a7..00000000 --- a/contracts/products/wNLP/WNlpRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./WNlpMidasAccessControlRoles.sol"; - -/** - * @title WNlpRedemptionVaultWithSwapper - * @notice Smart contract that handles wNLP redemptions - * @author RedDuck Software - */ -contract WNlpRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - WNlpMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return W_NLP_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/wNLP/wNLP.sol b/contracts/products/wNLP/wNLP.sol deleted file mode 100644 index 362b0aaf..00000000 --- a/contracts/products/wNLP/wNLP.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title wNLP - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract wNLP is mToken { - /** - * @notice actor that can mint wNLP - */ - bytes32 public constant W_NLP_MINT_OPERATOR_ROLE = - keccak256("W_NLP_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn wNLP - */ - bytes32 public constant W_NLP_BURN_OPERATOR_ROLE = - keccak256("W_NLP_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause wNLP - */ - bytes32 public constant W_NLP_PAUSE_OPERATOR_ROLE = - keccak256("W_NLP_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Nunch wNLP", "wNLP"); - } - - /** - * @dev AC role, owner of which can mint wNLP token - */ - function _minterRole() internal pure override returns (bytes32) { - return W_NLP_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn wNLP token - */ - function _burnerRole() internal pure override returns (bytes32) { - return W_NLP_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause wNLP token - */ - function _pauserRole() internal pure override returns (bytes32) { - return W_NLP_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/wVLP/WVLPCustomAggregatorFeed.sol b/contracts/products/wVLP/WVLPCustomAggregatorFeed.sol deleted file mode 100644 index 683e7fef..00000000 --- a/contracts/products/wVLP/WVLPCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./WVLPMidasAccessControlRoles.sol"; - -/** - * @title WVLPCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for wVLP, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract WVLPCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - WVLPMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return W_VLP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/wVLP/WVLPDataFeed.sol b/contracts/products/wVLP/WVLPDataFeed.sol deleted file mode 100644 index ebf61818..00000000 --- a/contracts/products/wVLP/WVLPDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./WVLPMidasAccessControlRoles.sol"; - -/** - * @title WVLPDataFeed - * @notice DataFeed for wVLP product - * @author RedDuck Software - */ -contract WVLPDataFeed is DataFeed, WVLPMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return W_VLP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/wVLP/WVLPDepositVault.sol b/contracts/products/wVLP/WVLPDepositVault.sol deleted file mode 100644 index bc83903b..00000000 --- a/contracts/products/wVLP/WVLPDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./WVLPMidasAccessControlRoles.sol"; - -/** - * @title WVLPDepositVault - * @notice Smart contract that handles wVLP minting - * @author RedDuck Software - */ -contract WVLPDepositVault is DepositVault, WVLPMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return W_VLP_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/wVLP/WVLPMidasAccessControlRoles.sol b/contracts/products/wVLP/WVLPMidasAccessControlRoles.sol deleted file mode 100644 index c41a48ee..00000000 --- a/contracts/products/wVLP/WVLPMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title WVLPMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for wVLP contracts - * @author RedDuck Software - */ -abstract contract WVLPMidasAccessControlRoles { - /** - * @notice actor that can manage WVLPDepositVault - */ - bytes32 public constant W_VLP_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("W_VLP_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage WVLPRedemptionVault - */ - bytes32 public constant W_VLP_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("W_VLP_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage WVLPCustomAggregatorFeed and WVLPDataFeed - */ - bytes32 public constant W_VLP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("W_VLP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/wVLP/WVLPRedemptionVaultWithSwapper.sol b/contracts/products/wVLP/WVLPRedemptionVaultWithSwapper.sol deleted file mode 100644 index f018f15a..00000000 --- a/contracts/products/wVLP/WVLPRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./WVLPMidasAccessControlRoles.sol"; - -/** - * @title WVLPRedemptionVaultWithSwapper - * @notice Smart contract that handles wVLP redemptions - * @author RedDuck Software - */ -contract WVLPRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - WVLPMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return W_VLP_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/wVLP/wVLP.sol b/contracts/products/wVLP/wVLP.sol deleted file mode 100644 index 3d1185c3..00000000 --- a/contracts/products/wVLP/wVLP.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title wVLP - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract wVLP is mToken { - /** - * @notice actor that can mint wVLP - */ - bytes32 public constant W_VLP_MINT_OPERATOR_ROLE = - keccak256("W_VLP_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn wVLP - */ - bytes32 public constant W_VLP_BURN_OPERATOR_ROLE = - keccak256("W_VLP_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause wVLP - */ - bytes32 public constant W_VLP_PAUSE_OPERATOR_ROLE = - keccak256("W_VLP_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Hyperbeat VLP", "wVLP"); - } - - /** - * @dev AC role, owner of which can mint wVLP token - */ - function _minterRole() internal pure override returns (bytes32) { - return W_VLP_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn wVLP token - */ - function _burnerRole() internal pure override returns (bytes32) { - return W_VLP_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause wVLP token - */ - function _pauserRole() internal pure override returns (bytes32) { - return W_VLP_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/weEUR/WeEurCustomAggregatorFeed.sol b/contracts/products/weEUR/WeEurCustomAggregatorFeed.sol deleted file mode 100644 index 83fdf77c..00000000 --- a/contracts/products/weEUR/WeEurCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./WeEurMidasAccessControlRoles.sol"; - -/** - * @title WeEurCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for weEUR, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract WeEurCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - WeEurMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return WE_EUR_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/weEUR/WeEurDataFeed.sol b/contracts/products/weEUR/WeEurDataFeed.sol deleted file mode 100644 index e2ae7ac6..00000000 --- a/contracts/products/weEUR/WeEurDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./WeEurMidasAccessControlRoles.sol"; - -/** - * @title WeEurDataFeed - * @notice DataFeed for weEUR product - * @author RedDuck Software - */ -contract WeEurDataFeed is DataFeed, WeEurMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return WE_EUR_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/weEUR/WeEurDepositVault.sol b/contracts/products/weEUR/WeEurDepositVault.sol deleted file mode 100644 index e678d59f..00000000 --- a/contracts/products/weEUR/WeEurDepositVault.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./WeEurMidasAccessControlRoles.sol"; - -/** - * @title WeEurDepositVault - * @notice Smart contract that handles weEUR minting - * @author RedDuck Software - */ -contract WeEurDepositVault is DepositVault, WeEurMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return WE_EUR_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/weEUR/WeEurMidasAccessControlRoles.sol b/contracts/products/weEUR/WeEurMidasAccessControlRoles.sol deleted file mode 100644 index 67882d53..00000000 --- a/contracts/products/weEUR/WeEurMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title WeEurMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for weEUR contracts - * @author RedDuck Software - */ -abstract contract WeEurMidasAccessControlRoles { - /** - * @notice actor that can manage WeEurDepositVault - */ - bytes32 public constant WE_EUR_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("WE_EUR_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage WeEurRedemptionVault - */ - bytes32 public constant WE_EUR_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("WE_EUR_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage WeEurCustomAggregatorFeed and WeEurDataFeed - */ - bytes32 public constant WE_EUR_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("WE_EUR_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/weEUR/WeEurRedemptionVaultWithSwapper.sol b/contracts/products/weEUR/WeEurRedemptionVaultWithSwapper.sol deleted file mode 100644 index d358c439..00000000 --- a/contracts/products/weEUR/WeEurRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./WeEurMidasAccessControlRoles.sol"; - -/** - * @title WeEurRedemptionVaultWithSwapper - * @notice Smart contract that handles weEUR redemptions - * @author RedDuck Software - */ -contract WeEurRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - WeEurMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return WE_EUR_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/weEUR/weEUR.sol b/contracts/products/weEUR/weEUR.sol deleted file mode 100644 index 8b34ef47..00000000 --- a/contracts/products/weEUR/weEUR.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title weEUR - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract weEUR is mToken { - /** - * @notice actor that can mint weEUR - */ - bytes32 public constant WE_EUR_MINT_OPERATOR_ROLE = - keccak256("WE_EUR_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn weEUR - */ - bytes32 public constant WE_EUR_BURN_OPERATOR_ROLE = - keccak256("WE_EUR_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause weEUR - */ - bytes32 public constant WE_EUR_PAUSE_OPERATOR_ROLE = - keccak256("WE_EUR_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("Liquid Euro", "weEUR"); - } - - /** - * @dev AC role, owner of which can mint weEUR token - */ - function _minterRole() internal pure override returns (bytes32) { - return WE_EUR_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn weEUR token - */ - function _burnerRole() internal pure override returns (bytes32) { - return WE_EUR_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause weEUR token - */ - function _pauserRole() internal pure override returns (bytes32) { - return WE_EUR_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/zeroGBTCV/ZeroGBtcvCustomAggregatorFeed.sol b/contracts/products/zeroGBTCV/ZeroGBtcvCustomAggregatorFeed.sol deleted file mode 100644 index 9c956568..00000000 --- a/contracts/products/zeroGBTCV/ZeroGBtcvCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./ZeroGBtcvMidasAccessControlRoles.sol"; - -/** - * @title ZeroGBtcvCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for zeroGBTCV, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract ZeroGBtcvCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - ZeroGBtcvMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return ZEROG_BTCV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/zeroGBTCV/ZeroGBtcvDataFeed.sol b/contracts/products/zeroGBTCV/ZeroGBtcvDataFeed.sol deleted file mode 100644 index a0cade96..00000000 --- a/contracts/products/zeroGBTCV/ZeroGBtcvDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./ZeroGBtcvMidasAccessControlRoles.sol"; - -/** - * @title ZeroGBtcvDataFeed - * @notice DataFeed for zeroGBTCV product - * @author RedDuck Software - */ -contract ZeroGBtcvDataFeed is DataFeed, ZeroGBtcvMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return ZEROG_BTCV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/zeroGBTCV/ZeroGBtcvDepositVault.sol b/contracts/products/zeroGBTCV/ZeroGBtcvDepositVault.sol deleted file mode 100644 index 6a676629..00000000 --- a/contracts/products/zeroGBTCV/ZeroGBtcvDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./ZeroGBtcvMidasAccessControlRoles.sol"; - -/** - * @title ZeroGBtcvDepositVault - * @notice Smart contract that handles zeroGBTCV minting - * @author RedDuck Software - */ -contract ZeroGBtcvDepositVault is - DepositVault, - ZeroGBtcvMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return ZEROG_BTCV_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/zeroGBTCV/ZeroGBtcvMidasAccessControlRoles.sol b/contracts/products/zeroGBTCV/ZeroGBtcvMidasAccessControlRoles.sol deleted file mode 100644 index dff21f5a..00000000 --- a/contracts/products/zeroGBTCV/ZeroGBtcvMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title ZeroGBtcvMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for zeroGBTCV contracts - * @author RedDuck Software - */ -abstract contract ZeroGBtcvMidasAccessControlRoles { - /** - * @notice actor that can manage ZeroGBtcvDepositVault - */ - bytes32 public constant ZEROG_BTCV_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("ZEROG_BTCV_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage ZeroGBtcvRedemptionVault - */ - bytes32 public constant ZEROG_BTCV_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("ZEROG_BTCV_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage ZeroGBtcvCustomAggregatorFeed and ZeroGBtcvDataFeed - */ - bytes32 public constant ZEROG_BTCV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("ZEROG_BTCV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/zeroGBTCV/ZeroGBtcvRedemptionVaultWithSwapper.sol b/contracts/products/zeroGBTCV/ZeroGBtcvRedemptionVaultWithSwapper.sol deleted file mode 100644 index 784b6138..00000000 --- a/contracts/products/zeroGBTCV/ZeroGBtcvRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./ZeroGBtcvMidasAccessControlRoles.sol"; - -/** - * @title ZeroGBtcvRedemptionVaultWithSwapper - * @notice Smart contract that handles zeroGBTCV redemptions - * @author RedDuck Software - */ -contract ZeroGBtcvRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - ZeroGBtcvMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return ZEROG_BTCV_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/zeroGBTCV/zeroGBTCV.sol b/contracts/products/zeroGBTCV/zeroGBTCV.sol deleted file mode 100644 index bfac1708..00000000 --- a/contracts/products/zeroGBTCV/zeroGBTCV.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title zeroGBTCV - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract zeroGBTCV is mToken { - /** - * @notice actor that can mint zeroGBTCV - */ - bytes32 public constant ZEROG_BTCV_MINT_OPERATOR_ROLE = - keccak256("ZEROG_BTCV_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn zeroGBTCV - */ - bytes32 public constant ZEROG_BTCV_BURN_OPERATOR_ROLE = - keccak256("ZEROG_BTCV_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause zeroGBTCV - */ - bytes32 public constant ZEROG_BTCV_PAUSE_OPERATOR_ROLE = - keccak256("ZEROG_BTCV_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("0G BTC Vault", "0gBTCV"); - } - - /** - * @dev AC role, owner of which can mint zeroGBTCV token - */ - function _minterRole() internal pure override returns (bytes32) { - return ZEROG_BTCV_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn zeroGBTCV token - */ - function _burnerRole() internal pure override returns (bytes32) { - return ZEROG_BTCV_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause zeroGBTCV token - */ - function _pauserRole() internal pure override returns (bytes32) { - return ZEROG_BTCV_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/zeroGETHV/ZeroGEthvCustomAggregatorFeed.sol b/contracts/products/zeroGETHV/ZeroGEthvCustomAggregatorFeed.sol deleted file mode 100644 index edea5652..00000000 --- a/contracts/products/zeroGETHV/ZeroGEthvCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./ZeroGEthvMidasAccessControlRoles.sol"; - -/** - * @title ZeroGEthvCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for zeroGETHV, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract ZeroGEthvCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - ZeroGEthvMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return ZEROG_ETHV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/zeroGETHV/ZeroGEthvDataFeed.sol b/contracts/products/zeroGETHV/ZeroGEthvDataFeed.sol deleted file mode 100644 index 692b0e03..00000000 --- a/contracts/products/zeroGETHV/ZeroGEthvDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./ZeroGEthvMidasAccessControlRoles.sol"; - -/** - * @title ZeroGEthvDataFeed - * @notice DataFeed for zeroGETHV product - * @author RedDuck Software - */ -contract ZeroGEthvDataFeed is DataFeed, ZeroGEthvMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return ZEROG_ETHV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/zeroGETHV/ZeroGEthvDepositVault.sol b/contracts/products/zeroGETHV/ZeroGEthvDepositVault.sol deleted file mode 100644 index 848f9dc0..00000000 --- a/contracts/products/zeroGETHV/ZeroGEthvDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./ZeroGEthvMidasAccessControlRoles.sol"; - -/** - * @title ZeroGEthvDepositVault - * @notice Smart contract that handles zeroGETHV minting - * @author RedDuck Software - */ -contract ZeroGEthvDepositVault is - DepositVault, - ZeroGEthvMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return ZEROG_ETHV_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/zeroGETHV/ZeroGEthvMidasAccessControlRoles.sol b/contracts/products/zeroGETHV/ZeroGEthvMidasAccessControlRoles.sol deleted file mode 100644 index 015393d0..00000000 --- a/contracts/products/zeroGETHV/ZeroGEthvMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title ZeroGEthvMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for zeroGETHV contracts - * @author RedDuck Software - */ -abstract contract ZeroGEthvMidasAccessControlRoles { - /** - * @notice actor that can manage ZeroGEthvDepositVault - */ - bytes32 public constant ZEROG_ETHV_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("ZEROG_ETHV_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage ZeroGEthvRedemptionVault - */ - bytes32 public constant ZEROG_ETHV_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("ZEROG_ETHV_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage ZeroGEthvCustomAggregatorFeed and ZeroGEthvDataFeed - */ - bytes32 public constant ZEROG_ETHV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("ZEROG_ETHV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/zeroGETHV/ZeroGEthvRedemptionVaultWithSwapper.sol b/contracts/products/zeroGETHV/ZeroGEthvRedemptionVaultWithSwapper.sol deleted file mode 100644 index 3f67d2a8..00000000 --- a/contracts/products/zeroGETHV/ZeroGEthvRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./ZeroGEthvMidasAccessControlRoles.sol"; - -/** - * @title ZeroGEthvRedemptionVaultWithSwapper - * @notice Smart contract that handles zeroGETHV redemptions - * @author RedDuck Software - */ -contract ZeroGEthvRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - ZeroGEthvMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return ZEROG_ETHV_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/zeroGETHV/zeroGETHV.sol b/contracts/products/zeroGETHV/zeroGETHV.sol deleted file mode 100644 index 1ed2a5b4..00000000 --- a/contracts/products/zeroGETHV/zeroGETHV.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title zeroGETHV - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract zeroGETHV is mToken { - /** - * @notice actor that can mint zeroGETHV - */ - bytes32 public constant ZEROG_ETHV_MINT_OPERATOR_ROLE = - keccak256("ZEROG_ETHV_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn zeroGETHV - */ - bytes32 public constant ZEROG_ETHV_BURN_OPERATOR_ROLE = - keccak256("ZEROG_ETHV_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause zeroGETHV - */ - bytes32 public constant ZEROG_ETHV_PAUSE_OPERATOR_ROLE = - keccak256("ZEROG_ETHV_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("0G ETH Vault", "0gETHV"); - } - - /** - * @dev AC role, owner of which can mint zeroGETHV token - */ - function _minterRole() internal pure override returns (bytes32) { - return ZEROG_ETHV_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn zeroGETHV token - */ - function _burnerRole() internal pure override returns (bytes32) { - return ZEROG_ETHV_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause zeroGETHV token - */ - function _pauserRole() internal pure override returns (bytes32) { - return ZEROG_ETHV_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/products/zeroGUSDV/ZeroGUsdvCustomAggregatorFeed.sol b/contracts/products/zeroGUSDV/ZeroGUsdvCustomAggregatorFeed.sol deleted file mode 100644 index e431100d..00000000 --- a/contracts/products/zeroGUSDV/ZeroGUsdvCustomAggregatorFeed.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; -import "./ZeroGUsdvMidasAccessControlRoles.sol"; - -/** - * @title ZeroGUsdvCustomAggregatorFeed - * @notice AggregatorV3 compatible feed for zeroGUSDV, - * where price is submitted manually by feed admins - * @author RedDuck Software - */ -contract ZeroGUsdvCustomAggregatorFeed is - CustomAggregatorV3CompatibleFeed, - ZeroGUsdvMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc CustomAggregatorV3CompatibleFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return ZEROG_USDV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/zeroGUSDV/ZeroGUsdvDataFeed.sol b/contracts/products/zeroGUSDV/ZeroGUsdvDataFeed.sol deleted file mode 100644 index cc3adf28..00000000 --- a/contracts/products/zeroGUSDV/ZeroGUsdvDataFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../feeds/DataFeed.sol"; -import "./ZeroGUsdvMidasAccessControlRoles.sol"; - -/** - * @title ZeroGUsdvDataFeed - * @notice DataFeed for zeroGUSDV product - * @author RedDuck Software - */ -contract ZeroGUsdvDataFeed is DataFeed, ZeroGUsdvMidasAccessControlRoles { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc DataFeed - */ - function feedAdminRole() public pure override returns (bytes32) { - return ZEROG_USDV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } -} diff --git a/contracts/products/zeroGUSDV/ZeroGUsdvDepositVault.sol b/contracts/products/zeroGUSDV/ZeroGUsdvDepositVault.sol deleted file mode 100644 index eff5efc5..00000000 --- a/contracts/products/zeroGUSDV/ZeroGUsdvDepositVault.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../DepositVault.sol"; -import "./ZeroGUsdvMidasAccessControlRoles.sol"; - -/** - * @title ZeroGUsdvDepositVault - * @notice Smart contract that handles zeroGUSDV minting - * @author RedDuck Software - */ -contract ZeroGUsdvDepositVault is - DepositVault, - ZeroGUsdvMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return ZEROG_USDV_DEPOSIT_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/zeroGUSDV/ZeroGUsdvMidasAccessControlRoles.sol b/contracts/products/zeroGUSDV/ZeroGUsdvMidasAccessControlRoles.sol deleted file mode 100644 index 432e4b76..00000000 --- a/contracts/products/zeroGUSDV/ZeroGUsdvMidasAccessControlRoles.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -/** - * @title ZeroGUsdvMidasAccessControlRoles - * @notice Base contract that stores all roles descriptors for zeroGUSDV contracts - * @author RedDuck Software - */ -abstract contract ZeroGUsdvMidasAccessControlRoles { - /** - * @notice actor that can manage ZeroGUsdvDepositVault - */ - bytes32 public constant ZEROG_USDV_DEPOSIT_VAULT_ADMIN_ROLE = - keccak256("ZEROG_USDV_DEPOSIT_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage ZeroGUsdvRedemptionVault - */ - bytes32 public constant ZEROG_USDV_REDEMPTION_VAULT_ADMIN_ROLE = - keccak256("ZEROG_USDV_REDEMPTION_VAULT_ADMIN_ROLE"); - - /** - * @notice actor that can manage ZeroGUsdvCustomAggregatorFeed and ZeroGUsdvDataFeed - */ - bytes32 public constant ZEROG_USDV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("ZEROG_USDV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); -} diff --git a/contracts/products/zeroGUSDV/ZeroGUsdvRedemptionVaultWithSwapper.sol b/contracts/products/zeroGUSDV/ZeroGUsdvRedemptionVaultWithSwapper.sol deleted file mode 100644 index d203a082..00000000 --- a/contracts/products/zeroGUSDV/ZeroGUsdvRedemptionVaultWithSwapper.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../RedemptionVaultWithSwapper.sol"; -import "./ZeroGUsdvMidasAccessControlRoles.sol"; - -/** - * @title ZeroGUsdvRedemptionVaultWithSwapper - * @notice Smart contract that handles zeroGUSDV redemptions - * @author RedDuck Software - */ -contract ZeroGUsdvRedemptionVaultWithSwapper is - RedemptionVaultWithSwapper, - ZeroGUsdvMidasAccessControlRoles -{ - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc ManageableVault - */ - function vaultRole() public pure override returns (bytes32) { - return ZEROG_USDV_REDEMPTION_VAULT_ADMIN_ROLE; - } -} diff --git a/contracts/products/zeroGUSDV/zeroGUSDV.sol b/contracts/products/zeroGUSDV/zeroGUSDV.sol deleted file mode 100644 index 3de5cebe..00000000 --- a/contracts/products/zeroGUSDV/zeroGUSDV.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../../mToken.sol"; - -/** - * @title zeroGUSDV - * @author RedDuck Software - */ -//solhint-disable contract-name-camelcase -contract zeroGUSDV is mToken { - /** - * @notice actor that can mint zeroGUSDV - */ - bytes32 public constant ZEROG_USDV_MINT_OPERATOR_ROLE = - keccak256("ZEROG_USDV_MINT_OPERATOR_ROLE"); - - /** - * @notice actor that can burn zeroGUSDV - */ - bytes32 public constant ZEROG_USDV_BURN_OPERATOR_ROLE = - keccak256("ZEROG_USDV_BURN_OPERATOR_ROLE"); - - /** - * @notice actor that can pause zeroGUSDV - */ - bytes32 public constant ZEROG_USDV_PAUSE_OPERATOR_ROLE = - keccak256("ZEROG_USDV_PAUSE_OPERATOR_ROLE"); - - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; - - /** - * @inheritdoc mToken - */ - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("0G USD Vault", "0gUSDV"); - } - - /** - * @dev AC role, owner of which can mint zeroGUSDV token - */ - function _minterRole() internal pure override returns (bytes32) { - return ZEROG_USDV_MINT_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can burn zeroGUSDV token - */ - function _burnerRole() internal pure override returns (bytes32) { - return ZEROG_USDV_BURN_OPERATOR_ROLE; - } - - /** - * @dev AC role, owner of which can pause zeroGUSDV token - */ - function _pauserRole() internal pure override returns (bytes32) { - return ZEROG_USDV_PAUSE_OPERATOR_ROLE; - } -} diff --git a/contracts/testers/BlacklistableTester.sol b/contracts/testers/BlacklistableTester.sol index eb4b64af..a037b87d 100644 --- a/contracts/testers/BlacklistableTester.sol +++ b/contracts/testers/BlacklistableTester.sol @@ -1,19 +1,11 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../access/Blacklistable.sol"; contract BlacklistableTester is Blacklistable { function initialize(address _accessControl) external initializer { - __Blacklistable_init(_accessControl); - } - - function initializeWithoutInitializer(address _accessControl) external { - __Blacklistable_init(_accessControl); - } - - function initializeUnchainedWithoutInitializer() external { - __Blacklistable_init_unchained(); + __WithMidasAccessControl_init(_accessControl); } function onlyNotBlacklistedTester(address account) @@ -21,5 +13,11 @@ contract BlacklistableTester is Blacklistable { onlyNotBlacklisted(account) {} + function contractAdminRole() public pure override returns (bytes32) { + return _DEFAULT_ADMIN_ROLE; + } + function _disableInitializers() internal override {} + + function _onlyProxyAdmin() internal view override {} } diff --git a/contracts/testers/CompositeDataFeedTest.sol b/contracts/testers/CompositeDataFeedTest.sol index 59c31b44..183c00b3 100644 --- a/contracts/testers/CompositeDataFeedTest.sol +++ b/contracts/testers/CompositeDataFeedTest.sol @@ -1,8 +1,12 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../feeds/CompositeDataFeed.sol"; contract CompositeDataFeedTest is CompositeDataFeed { + constructor() CompositeDataFeed(_DEFAULT_ADMIN_ROLE) {} + function _disableInitializers() internal override {} + + function _onlyProxyAdmin() internal view override {} } diff --git a/contracts/testers/CustomAggregatorV3CompatibleFeedAdjustedTester.sol b/contracts/testers/CustomAggregatorV3CompatibleFeedAdjustedTester.sol index c58fda64..d7f8d23a 100644 --- a/contracts/testers/CustomAggregatorV3CompatibleFeedAdjustedTester.sol +++ b/contracts/testers/CustomAggregatorV3CompatibleFeedAdjustedTester.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../feeds/CustomAggregatorV3CompatibleFeedAdjusted.sol"; diff --git a/contracts/testers/CustomAggregatorV3CompatibleFeedGrowthTester.sol b/contracts/testers/CustomAggregatorV3CompatibleFeedGrowthTester.sol index f7de4053..50578a44 100644 --- a/contracts/testers/CustomAggregatorV3CompatibleFeedGrowthTester.sol +++ b/contracts/testers/CustomAggregatorV3CompatibleFeedGrowthTester.sol @@ -1,23 +1,20 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../feeds/CustomAggregatorV3CompatibleFeedGrowth.sol"; contract CustomAggregatorV3CompatibleFeedGrowthTester is CustomAggregatorV3CompatibleFeedGrowth { - bytes32 public constant CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); + constructor() + CustomAggregatorV3CompatibleFeedGrowth( + keccak256("CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE") + ) + {} function _disableInitializers() internal override {} - function feedAdminRole() public pure override returns (bytes32) { - return CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } - - function setMaxAnswerDeviation(uint256 _deviation) public { - maxAnswerDeviation = _deviation; - } + function _onlyProxyAdmin() internal view override {} function getDeviation( int256 _lastPrice, diff --git a/contracts/testers/CustomAggregatorV3CompatibleFeedTester.sol b/contracts/testers/CustomAggregatorV3CompatibleFeedTester.sol index 25ad6944..ef38f00f 100644 --- a/contracts/testers/CustomAggregatorV3CompatibleFeedTester.sol +++ b/contracts/testers/CustomAggregatorV3CompatibleFeedTester.sol @@ -1,19 +1,20 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../feeds/CustomAggregatorV3CompatibleFeed.sol"; contract CustomAggregatorV3CompatibleFeedTester is CustomAggregatorV3CompatibleFeed { - bytes32 public constant CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = - keccak256("CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); + constructor() + CustomAggregatorV3CompatibleFeed( + keccak256("CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE") + ) + {} function _disableInitializers() internal override {} - function feedAdminRole() public pure override returns (bytes32) { - return CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE; - } + function _onlyProxyAdmin() internal view override {} function getDeviation(int256 _lastPrice, int256 _newPrice) public diff --git a/contracts/testers/DataFeedTest.sol b/contracts/testers/DataFeedTest.sol index f67b8db3..38d9eba3 100644 --- a/contracts/testers/DataFeedTest.sol +++ b/contracts/testers/DataFeedTest.sol @@ -1,8 +1,12 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../feeds/DataFeed.sol"; contract DataFeedTest is DataFeed { + constructor() DataFeed(_DEFAULT_ADMIN_ROLE) {} + function _disableInitializers() internal override {} + + function _onlyProxyAdmin() internal view override {} } diff --git a/contracts/testers/DecimalsCorrectionTester.sol b/contracts/testers/DecimalsCorrectionTester.sol index 73b7e251..697debc1 100644 --- a/contracts/testers/DecimalsCorrectionTester.sol +++ b/contracts/testers/DecimalsCorrectionTester.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../libraries/DecimalsCorrectionLibrary.sol"; diff --git a/contracts/testers/DepositVaultTest.sol b/contracts/testers/DepositVaultTest.sol index 8f1df7b8..fb6b88f2 100644 --- a/contracts/testers/DepositVaultTest.sol +++ b/contracts/testers/DepositVaultTest.sol @@ -1,39 +1,36 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "../DepositVault.sol"; - -contract DepositVaultTest is DepositVault { - bool private _overrideGetTokenRate; - uint256 private _getTokenRateValue; - - function _disableInitializers() internal override {} +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - function tokenTransferFromToTester( - address token, - address from, - address to, - uint256 amount, - uint256 tokenDecimals - ) external { - _tokenTransferFromTo(token, from, to, amount, tokenDecimals); - } +import "../DepositVault.sol"; +import "../libraries/MidasAuthLibrary.sol"; +import {ManageableVaultTesterBase} from "./ManageableVaultTester.sol"; - function tokenTransferToUserTester( - address token, - address to, - uint256 amount, - uint256 tokenDecimals - ) external { - _tokenTransferToUser(token, to, amount, tokenDecimals); - } +abstract contract DepositVaultTestBase is + DepositVault, + ManageableVaultTesterBase +{ + function _disableInitializers() + internal + virtual + override(Initializable, ManageableVaultTesterBase) + {} - function setOverrideGetTokenRate(bool val) external { - _overrideGetTokenRate = val; + function convertTokenToUsdTest(address tokenIn, uint256 amount) + external + view + returns (uint256 amountInUsd, uint256 rate) + { + return _convertTokenToUsd(tokenIn, amount); } - function setGetTokenRateValue(uint256 val) external { - _getTokenRateValue = val; + function convertUsdToMTokenTest(uint256 amountUsd) + external + view + returns (uint256 amountMToken, uint256 mTokenRate) + { + return _convertUsdToMToken(amountUsd); } function calcAndValidateDeposit( @@ -45,30 +42,55 @@ contract DepositVaultTest is DepositVault { return _calcAndValidateDeposit(user, tokenIn, amountToken, isInstant); } - function convertTokenToUsdTest(address tokenIn, uint256 amount) - external - returns (uint256 amountInUsd, uint256 rate) - { - return _convertTokenToUsd(tokenIn, amount); - } - - function convertUsdToMTokenTest(uint256 amountUsd) - external - returns (uint256 amountMToken, uint256 mTokenRate) - { - return _convertUsdToMToken(amountUsd); + function calculateHoldbackPartRateFromAvgTest( + uint256 depositedUsdAmount, + uint256 depositedInstantUsdAmount, + uint256 mTokenRate, + uint256 avgMTokenRate + ) external pure returns (uint256) { + return + _calculateHoldbackPartRateFromAvg( + Request({ + depositedInstantUsdAmount: depositedInstantUsdAmount, + tokenOutRate: mTokenRate, + approvedTokenOutRate: 0, + depositedUsdAmount: depositedUsdAmount, + usdAmountWithoutFees: 0, + recipient: address(0), + tokenIn: address(0), + status: RequestStatus.Pending, + amountMToken: 0 + }), + avgMTokenRate + ); } function _getTokenRate(address dataFeed, bool stable) internal view - override + virtual + override(ManageableVaultTesterBase, ManageableVault) returns (uint256) { - if (_overrideGetTokenRate) { - return _getTokenRateValue; - } + return ManageableVaultTesterBase._getTokenRate(dataFeed, stable); + } - return super._getTokenRate(dataFeed, stable); + function contractAdminRole() + public + view + virtual + override(ManageableVaultTesterBase, ManageableVault) + returns (bytes32) + { + return ManageableVault.contractAdminRole(); } } + +contract DepositVaultTest is DepositVaultTestBase { + constructor() + DepositVault( + keccak256("DEPOSIT_VAULT_ADMIN_ROLE"), + MidasAuthLibrary.DEFAULT_GREENLISTED_ROLE + ) + {} +} diff --git a/contracts/testers/DepositVaultWithAaveTest.sol b/contracts/testers/DepositVaultWithAaveTest.sol index a1c0c958..dc02ed85 100644 --- a/contracts/testers/DepositVaultWithAaveTest.sol +++ b/contracts/testers/DepositVaultWithAaveTest.sol @@ -1,74 +1,68 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "../DepositVaultWithAave.sol"; - -contract DepositVaultWithAaveTest is DepositVaultWithAave { - bool private _overrideGetTokenRate; - uint256 private _getTokenRateValue; - - function _disableInitializers() internal override {} - - function tokenTransferFromToTester( - address token, - address from, - address to, - uint256 amount, - uint256 tokenDecimals - ) external { - _tokenTransferFromTo(token, from, to, amount, tokenDecimals); - } +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - function tokenTransferToUserTester( - address token, - address to, - uint256 amount, - uint256 tokenDecimals - ) external { - _tokenTransferToUser(token, to, amount, tokenDecimals); - } +import "../DepositVaultWithAave.sol"; +import "./DepositVaultTest.sol"; - function setOverrideGetTokenRate(bool val) external { - _overrideGetTokenRate = val; - } +contract DepositVaultWithAaveTest is + DepositVaultTestBase, + DepositVaultWithAave +{ + constructor() + DepositVaultWithAave( + keccak256("DEPOSIT_VAULT_ADMIN_ROLE"), + MidasAuthLibrary.DEFAULT_GREENLISTED_ROLE + ) + {} - function setGetTokenRateValue(uint256 val) external { - _getTokenRateValue = val; + function _disableInitializers() + internal + override(Initializable, DepositVaultTestBase) + { + DepositVaultTestBase._disableInitializers(); } - function calcAndValidateDeposit( - address user, + function _instantTransferTokensToTokensReceiver( address tokenIn, uint256 amountToken, - bool isInstant - ) external returns (CalcAndValidateDepositResult memory) { - return _calcAndValidateDeposit(user, tokenIn, amountToken, isInstant); + uint256 tokensDecimals + ) internal virtual override(DepositVaultWithAave, DepositVault) { + DepositVaultWithAave._instantTransferTokensToTokensReceiver( + tokenIn, + amountToken, + tokensDecimals + ); } - function convertTokenToUsdTest(address tokenIn, uint256 amount) - external - returns (uint256 amountInUsd, uint256 rate) - { - return _convertTokenToUsd(tokenIn, amount); - } - - function convertUsdToMTokenTest(uint256 amountUsd) - external - returns (uint256 amountMToken, uint256 mTokenRate) - { - return _convertUsdToMToken(amountUsd); + function _requestTransferTokensToTokensReceiver( + address tokenIn, + uint256 amountToken, + uint256 tokensDecimals + ) internal override(DepositVaultWithAave, DepositVault) { + DepositVaultWithAave._requestTransferTokensToTokensReceiver( + tokenIn, + amountToken, + tokensDecimals + ); } function _getTokenRate(address dataFeed, bool stable) internal view - override + override(DepositVaultTestBase, ManageableVault) returns (uint256) { - if (_overrideGetTokenRate) { - return _getTokenRateValue; - } + return DepositVaultTestBase._getTokenRate(dataFeed, stable); + } - return super._getTokenRate(dataFeed, stable); + function contractAdminRole() + public + view + override(DepositVaultTestBase, ManageableVault) + returns (bytes32) + { + return DepositVaultTestBase.contractAdminRole(); } } diff --git a/contracts/testers/DepositVaultWithMTokenTest.sol b/contracts/testers/DepositVaultWithMTokenTest.sol index c07b431c..f4917042 100644 --- a/contracts/testers/DepositVaultWithMTokenTest.sol +++ b/contracts/testers/DepositVaultWithMTokenTest.sol @@ -1,74 +1,68 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "../DepositVaultWithMToken.sol"; - -contract DepositVaultWithMTokenTest is DepositVaultWithMToken { - bool private _overrideGetTokenRate; - uint256 private _getTokenRateValue; - - function _disableInitializers() internal override {} - - function tokenTransferFromToTester( - address token, - address from, - address to, - uint256 amount, - uint256 tokenDecimals - ) external { - _tokenTransferFromTo(token, from, to, amount, tokenDecimals); - } +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - function tokenTransferToUserTester( - address token, - address to, - uint256 amount, - uint256 tokenDecimals - ) external { - _tokenTransferToUser(token, to, amount, tokenDecimals); - } +import "../DepositVaultWithMToken.sol"; +import "./DepositVaultTest.sol"; - function setOverrideGetTokenRate(bool val) external { - _overrideGetTokenRate = val; - } +contract DepositVaultWithMTokenTest is + DepositVaultTestBase, + DepositVaultWithMToken +{ + constructor() + DepositVaultWithMToken( + keccak256("DEPOSIT_VAULT_ADMIN_ROLE"), + MidasAuthLibrary.DEFAULT_GREENLISTED_ROLE + ) + {} - function setGetTokenRateValue(uint256 val) external { - _getTokenRateValue = val; + function _disableInitializers() + internal + override(Initializable, DepositVaultTestBase) + { + DepositVaultTestBase._disableInitializers(); } - function calcAndValidateDeposit( - address user, + function _instantTransferTokensToTokensReceiver( address tokenIn, uint256 amountToken, - bool isInstant - ) external returns (CalcAndValidateDepositResult memory) { - return _calcAndValidateDeposit(user, tokenIn, amountToken, isInstant); + uint256 tokensDecimals + ) internal virtual override(DepositVaultWithMToken, DepositVault) { + DepositVaultWithMToken._instantTransferTokensToTokensReceiver( + tokenIn, + amountToken, + tokensDecimals + ); } - function convertTokenToUsdTest(address tokenIn, uint256 amount) - external - returns (uint256 amountInUsd, uint256 rate) - { - return _convertTokenToUsd(tokenIn, amount); - } - - function convertUsdToMTokenTest(uint256 amountUsd) - external - returns (uint256 amountMToken, uint256 mTokenRate) - { - return _convertUsdToMToken(amountUsd); + function _requestTransferTokensToTokensReceiver( + address tokenIn, + uint256 amountToken, + uint256 tokensDecimals + ) internal override(DepositVaultWithMToken, DepositVault) { + DepositVaultWithMToken._requestTransferTokensToTokensReceiver( + tokenIn, + amountToken, + tokensDecimals + ); } function _getTokenRate(address dataFeed, bool stable) internal view - override + override(DepositVaultTestBase, ManageableVault) returns (uint256) { - if (_overrideGetTokenRate) { - return _getTokenRateValue; - } + return DepositVaultTestBase._getTokenRate(dataFeed, stable); + } - return super._getTokenRate(dataFeed, stable); + function contractAdminRole() + public + view + override(DepositVaultTestBase, ManageableVault) + returns (bytes32) + { + return DepositVaultTestBase.contractAdminRole(); } } diff --git a/contracts/testers/DepositVaultWithMorphoTest.sol b/contracts/testers/DepositVaultWithMorphoTest.sol index dd282045..790e3c77 100644 --- a/contracts/testers/DepositVaultWithMorphoTest.sol +++ b/contracts/testers/DepositVaultWithMorphoTest.sol @@ -1,74 +1,68 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "../DepositVaultWithMorpho.sol"; - -contract DepositVaultWithMorphoTest is DepositVaultWithMorpho { - bool private _overrideGetTokenRate; - uint256 private _getTokenRateValue; - - function _disableInitializers() internal override {} - - function tokenTransferFromToTester( - address token, - address from, - address to, - uint256 amount, - uint256 tokenDecimals - ) external { - _tokenTransferFromTo(token, from, to, amount, tokenDecimals); - } +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - function tokenTransferToUserTester( - address token, - address to, - uint256 amount, - uint256 tokenDecimals - ) external { - _tokenTransferToUser(token, to, amount, tokenDecimals); - } +import "../DepositVaultWithMorpho.sol"; +import "./DepositVaultTest.sol"; - function setOverrideGetTokenRate(bool val) external { - _overrideGetTokenRate = val; - } +contract DepositVaultWithMorphoTest is + DepositVaultTestBase, + DepositVaultWithMorpho +{ + constructor() + DepositVaultWithMorpho( + keccak256("DEPOSIT_VAULT_ADMIN_ROLE"), + MidasAuthLibrary.DEFAULT_GREENLISTED_ROLE + ) + {} - function setGetTokenRateValue(uint256 val) external { - _getTokenRateValue = val; + function _disableInitializers() + internal + override(Initializable, DepositVaultTestBase) + { + DepositVaultTestBase._disableInitializers(); } - function calcAndValidateDeposit( - address user, + function _instantTransferTokensToTokensReceiver( address tokenIn, uint256 amountToken, - bool isInstant - ) external returns (CalcAndValidateDepositResult memory) { - return _calcAndValidateDeposit(user, tokenIn, amountToken, isInstant); + uint256 tokensDecimals + ) internal virtual override(DepositVaultWithMorpho, DepositVault) { + DepositVaultWithMorpho._instantTransferTokensToTokensReceiver( + tokenIn, + amountToken, + tokensDecimals + ); } - function convertTokenToUsdTest(address tokenIn, uint256 amount) - external - returns (uint256 amountInUsd, uint256 rate) - { - return _convertTokenToUsd(tokenIn, amount); - } - - function convertUsdToMTokenTest(uint256 amountUsd) - external - returns (uint256 amountMToken, uint256 mTokenRate) - { - return _convertUsdToMToken(amountUsd); + function _requestTransferTokensToTokensReceiver( + address tokenIn, + uint256 amountToken, + uint256 tokensDecimals + ) internal override(DepositVaultWithMorpho, DepositVault) { + DepositVaultWithMorpho._requestTransferTokensToTokensReceiver( + tokenIn, + amountToken, + tokensDecimals + ); } function _getTokenRate(address dataFeed, bool stable) internal view - override + override(DepositVaultTestBase, ManageableVault) returns (uint256) { - if (_overrideGetTokenRate) { - return _getTokenRateValue; - } + return DepositVaultTestBase._getTokenRate(dataFeed, stable); + } - return super._getTokenRate(dataFeed, stable); + function contractAdminRole() + public + view + override(DepositVaultTestBase, ManageableVault) + returns (bytes32) + { + return DepositVaultTestBase.contractAdminRole(); } } diff --git a/contracts/testers/DepositVaultWithUSTBTest.sol b/contracts/testers/DepositVaultWithUSTBTest.sol index 995989c3..f8c5fbd9 100644 --- a/contracts/testers/DepositVaultWithUSTBTest.sol +++ b/contracts/testers/DepositVaultWithUSTBTest.sol @@ -1,74 +1,56 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "../DepositVaultWithUSTB.sol"; - -contract DepositVaultWithUSTBTest is DepositVaultWithUSTB { - bool private _overrideGetTokenRate; - uint256 private _getTokenRateValue; - - function _disableInitializers() internal override {} +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - function tokenTransferFromToTester( - address token, - address from, - address to, - uint256 amount, - uint256 tokenDecimals - ) external { - _tokenTransferFromTo(token, from, to, amount, tokenDecimals); - } - - function tokenTransferToUserTester( - address token, - address to, - uint256 amount, - uint256 tokenDecimals - ) external { - _tokenTransferToUser(token, to, amount, tokenDecimals); - } - - function setOverrideGetTokenRate(bool val) external { - _overrideGetTokenRate = val; - } - - function setGetTokenRateValue(uint256 val) external { - _getTokenRateValue = val; +import "../DepositVaultWithUSTB.sol"; +import "./DepositVaultTest.sol"; + +contract DepositVaultWithUSTBTest is + DepositVaultTestBase, + DepositVaultWithUSTB +{ + constructor() + DepositVaultWithUSTB( + keccak256("DEPOSIT_VAULT_ADMIN_ROLE"), + MidasAuthLibrary.DEFAULT_GREENLISTED_ROLE + ) + {} + + function _disableInitializers() + internal + override(Initializable, DepositVaultTestBase) + { + DepositVaultTestBase._disableInitializers(); } - function calcAndValidateDeposit( - address user, + function _instantTransferTokensToTokensReceiver( address tokenIn, uint256 amountToken, - bool isInstant - ) external returns (CalcAndValidateDepositResult memory) { - return _calcAndValidateDeposit(user, tokenIn, amountToken, isInstant); - } - - function convertTokenToUsdTest(address tokenIn, uint256 amount) - external - returns (uint256 amountInUsd, uint256 rate) - { - return _convertTokenToUsd(tokenIn, amount); - } - - function convertUsdToMTokenTest(uint256 amountUsd) - external - returns (uint256 amountMToken, uint256 mTokenRate) - { - return _convertUsdToMToken(amountUsd); + uint256 tokensDecimals + ) internal virtual override(DepositVaultWithUSTB, DepositVault) { + DepositVaultWithUSTB._instantTransferTokensToTokensReceiver( + tokenIn, + amountToken, + tokensDecimals + ); } function _getTokenRate(address dataFeed, bool stable) internal view - override + override(DepositVaultTestBase, ManageableVault) returns (uint256) { - if (_overrideGetTokenRate) { - return _getTokenRateValue; - } + return DepositVaultTestBase._getTokenRate(dataFeed, stable); + } - return super._getTokenRate(dataFeed, stable); + function contractAdminRole() + public + view + override(DepositVaultTestBase, ManageableVault) + returns (bytes32) + { + return DepositVaultTestBase.contractAdminRole(); } } diff --git a/contracts/testers/GreenlistableTester.sol b/contracts/testers/GreenlistableTester.sol index 434fdc0e..1b7cc74b 100644 --- a/contracts/testers/GreenlistableTester.sol +++ b/contracts/testers/GreenlistableTester.sol @@ -1,19 +1,11 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../access/Greenlistable.sol"; contract GreenlistableTester is Greenlistable { function initialize(address _accessControl) external initializer { - __Greenlistable_init(_accessControl); - } - - function initializeWithoutInitializer(address _accessControl) external { - __Greenlistable_init(_accessControl); - } - - function initializeUnchainedWithoutInitializer() external { - __Greenlistable_init_unchained(); + __WithMidasAccessControl_init(_accessControl); } function onlyGreenlistedTester(address account) @@ -21,19 +13,19 @@ contract GreenlistableTester is Greenlistable { onlyGreenlisted(account) {} - function onlyGreenlistTogglerTester(address account) external view { - _onlyGreenlistToggler(account); + function _disableInitializers() internal override {} + + function _onlyProxyAdmin() internal view override {} + + function greenlistAdminRole() public view virtual returns (bytes32) { + return keccak256("GREENLIST_ADMIN_ROLE"); } - function _disableInitializers() internal override {} + function contractAdminRole() public pure override returns (bytes32) { + return _DEFAULT_ADMIN_ROLE; + } - function greenlistTogglerRole() - public - view - virtual - override - returns (bytes32) - { - return keccak256("GREENLIST_TOGGLER_ROLE"); + function greenlistedRole() public view virtual override returns (bytes32) { + return MidasAuthLibrary.DEFAULT_GREENLISTED_ROLE; } } diff --git a/contracts/testers/ManageableVaultTester.sol b/contracts/testers/ManageableVaultTester.sol index d9d245bc..fe348eae 100644 --- a/contracts/testers/ManageableVaultTester.sol +++ b/contracts/testers/ManageableVaultTester.sol @@ -1,60 +1,87 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../abstract/ManageableVault.sol"; +import "../libraries/MidasAuthLibrary.sol"; -contract ManageableVaultTester is ManageableVault { - function _disableInitializers() internal override {} - - function initialize( - address _ac, - MTokenInitParams calldata _mTokenInitParams, - ReceiversInitParams calldata _receiversInitParams, - InstantInitParams calldata _instantInitParams, - address _sanctionsList, - uint256 _variationTolerance, - uint256 _minAmount +abstract contract ManageableVaultTesterBase is ManageableVault { + bytes32 private _contractAdminRoleOverride; + bool private _overrideGetTokenRate; + uint256 private _getTokenRateValue; + + function _disableInitializers() internal virtual override {} + + function setVaultRole(bytes32 role) external { + _contractAdminRoleOverride = role; + } + + function setOverrideGetTokenRate(bool _override) external { + _overrideGetTokenRate = _override; + } + + function tokenTransferFromToTester( + address token, + address from, + address to, + uint256 amount, + uint256 tokenDecimals + ) external { + _tokenTransferFromTo(token, from, to, amount, tokenDecimals); + } + + function setGetTokenRateValue(uint256 val) external { + _getTokenRateValue = val; + } + + function _getTokenRate(address dataFeed, bool stable) + internal + view + virtual + override + returns (uint256) + { + if (_overrideGetTokenRate) { + return _getTokenRateValue; + } + + return super._getTokenRate(dataFeed, stable); + } + + function initializeExternal( + CommonVaultInitParams calldata _commonVaultInitParams ) external initializer { - __ManageableVault_init( - _ac, - _mTokenInitParams, - _receiversInitParams, - _instantInitParams, - _sanctionsList, - _variationTolerance, - _minAmount - ); + __ManageableVault_init(_commonVaultInitParams); } function initializeWithoutInitializer( - address _ac, - MTokenInitParams calldata _mTokenInitParams, - ReceiversInitParams calldata _receiversInitParams, - InstantInitParams calldata _instantInitParams, - address _sanctionsList, - uint256 _variationTolerance, - uint256 _minAmount + CommonVaultInitParams calldata _commonVaultInitParams ) external { - __ManageableVault_init( - _ac, - _mTokenInitParams, - _receiversInitParams, - _instantInitParams, - _sanctionsList, - _variationTolerance, - _minAmount - ); + __ManageableVault_init(_commonVaultInitParams); } - function vaultRole() public view virtual override returns (bytes32) {} - - function greenlistTogglerRole() + function contractAdminRole() public view virtual override returns (bytes32) { - return keccak256("GREENLIST_TOGGLER_ROLE"); + if (_contractAdminRoleOverride != bytes32(0)) { + return _contractAdminRoleOverride; + } + + return keccak256("VAULT_ADMIN_ROLE"); } } + +contract ManageableVaultTester is ManageableVaultTesterBase { + /** + * @notice constructor + */ + constructor() + ManageableVault( + keccak256("VAULT_ADMIN_ROLE"), + MidasAuthLibrary.DEFAULT_GREENLISTED_ROLE + ) + {} +} diff --git a/contracts/testers/MidasAccessControlTest.sol b/contracts/testers/MidasAccessControlTest.sol index 59347595..eb3b4949 100644 --- a/contracts/testers/MidasAccessControlTest.sol +++ b/contracts/testers/MidasAccessControlTest.sol @@ -1,8 +1,14 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../access/MidasAccessControl.sol"; contract MidasAccessControlTest is MidasAccessControl { function _disableInitializers() internal override {} + + function _onlyProxyAdmin() internal view override {} + + function setDefaultDelayTest(uint32 delay) external { + defaultDelay = delay; + } } diff --git a/contracts/testers/MidasAccessControlTimelockControllerTest.sol b/contracts/testers/MidasAccessControlTimelockControllerTest.sol new file mode 100644 index 00000000..e978f84a --- /dev/null +++ b/contracts/testers/MidasAccessControlTimelockControllerTest.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; + +import "../access/MidasAccessControlTimelockController.sol"; + +contract MidasAccessControlTimelockControllerTest is + MidasAccessControlTimelockController +{ + function _disableInitializers() internal override {} + + function _onlyProxyAdmin() internal view override {} +} diff --git a/contracts/testers/MidasAxelarVaultExecutableTester.sol b/contracts/testers/MidasAxelarVaultExecutableTester.sol index df49dd7d..75770d9c 100644 --- a/contracts/testers/MidasAxelarVaultExecutableTester.sol +++ b/contracts/testers/MidasAxelarVaultExecutableTester.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.21; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../misc/axelar/MidasAxelarVaultExecutable.sol"; diff --git a/contracts/testers/MidasInitializableTester.sol b/contracts/testers/MidasInitializableTester.sol new file mode 100644 index 00000000..f6d97c08 --- /dev/null +++ b/contracts/testers/MidasInitializableTester.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; + +import {MidasInitializable} from "../abstract/MidasInitializable.sol"; + +contract MidasInitializableTester is MidasInitializable { + uint256 public initializeCallsCount; + + uint256 public reinitCallsCount; + + function initialize() external { + _initializeV1(); + initializeV2(); + } + + function initializeV2() public reinitializer(2) onlyProxyAdmin { + reinitCallsCount++; + } + + function _initializeV1() private initializer { + initializeCallsCount++; + } +} diff --git a/contracts/testers/MidasLzVaultComposerSyncTester.sol b/contracts/testers/MidasLzVaultComposerSyncTester.sol index 5a820b28..bcce73d0 100644 --- a/contracts/testers/MidasLzVaultComposerSyncTester.sol +++ b/contracts/testers/MidasLzVaultComposerSyncTester.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.21; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../misc/layerzero/MidasLzVaultComposerSync.sol"; diff --git a/contracts/testers/MidasPauseManagerTest.sol b/contracts/testers/MidasPauseManagerTest.sol new file mode 100644 index 00000000..4290f1e4 --- /dev/null +++ b/contracts/testers/MidasPauseManagerTest.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; + +import "../access/MidasPauseManager.sol"; + +contract MidasPauseManagerTest is MidasPauseManager { + function _disableInitializers() internal override {} + + function _onlyProxyAdmin() internal view override {} +} diff --git a/contracts/testers/MidasTimelockManagerTest.sol b/contracts/testers/MidasTimelockManagerTest.sol new file mode 100644 index 00000000..3b101490 --- /dev/null +++ b/contracts/testers/MidasTimelockManagerTest.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; + +import "../access/MidasTimelockManager.sol"; + +contract MidasTimelockManagerTest is MidasTimelockManager { + function _disableInitializers() internal override {} + + function _onlyProxyAdmin() internal view override {} +} diff --git a/contracts/testers/PausableTester.sol b/contracts/testers/PausableTester.sol index 40cf87ba..fb769363 100644 --- a/contracts/testers/PausableTester.sol +++ b/contracts/testers/PausableTester.sol @@ -1,20 +1,33 @@ -// SPDX-License-Identifier: UNLICENSEDI -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "../access/Pausable.sol"; +import {WithMidasAccessControl} from "../access/WithMidasAccessControl.sol"; +import {PauseGuardsLibrary} from "../libraries/PauseGuardsLibrary.sol"; + +contract PausableTester is WithMidasAccessControl { + bytes32 private _contractAdminRoleOverride; + + function setContractAdminRole(bytes32 role) external { + _contractAdminRoleOverride = role; + } -contract PausableTester is Pausable { function initialize(address _accessControl) external initializer { - __Pausable_init(_accessControl); + __WithMidasAccessControl_init(_accessControl); } - function initializeWithoutInitializer(address _accessControl) external { - __Pausable_init(_accessControl); + function requireFnNotPaused(bytes4 fn) external { + PauseGuardsLibrary.requireFnNotPaused(accessControl, fn); } - function pauseAdminRole() public view override returns (bytes32) { - return accessControl.DEFAULT_ADMIN_ROLE(); + function requireNotPaused(bytes4 fn) external { + PauseGuardsLibrary.requireNotPaused(accessControl, fn); + } + + function contractAdminRole() public view override returns (bytes32) { + return _contractAdminRoleOverride; } function _disableInitializers() internal override {} + + function _onlyProxyAdmin() internal view override {} } diff --git a/contracts/testers/RateLimitLibraryTester.sol b/contracts/testers/RateLimitLibraryTester.sol new file mode 100644 index 00000000..c8af8005 --- /dev/null +++ b/contracts/testers/RateLimitLibraryTester.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; + +import {EnumerableSetUpgradeable as EnumerableSet} from "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; + +import {RateLimitLibrary} from "../libraries/RateLimitLibrary.sol"; + +/** + * @notice Exposes {RateLimitLibrary} internals for unit tests. + */ +contract RateLimitLibraryTester { + using EnumerableSet for EnumerableSet.UintSet; + + RateLimitLibrary.WindowRateLimits private _limits; + + function setWindowLimitPublic(uint256 window, uint256 limit) + external + returns (uint256 previousLimit) + { + return RateLimitLibrary.setWindowLimit(_limits, window, limit); + } + + function removeWindowLimitPublic(uint256 window) external { + RateLimitLibrary.removeWindowLimit(_limits, window); + } + + function consumeLimitPublic(uint256 amount) external { + RateLimitLibrary.consumeLimit(_limits, amount); + } + + function getWindowStatusesPublic() + external + view + returns (RateLimitLibrary.WindowRateLimitStatus[] memory) + { + return RateLimitLibrary.getWindowStatuses(_limits); + } + + function getWindowConfigPublic(uint256 window) + external + view + returns ( + uint256 limit, + uint256 amountInFlight, + uint256 lastUpdated, + uint256 windowDuration + ) + { + RateLimitLibrary.WindowRateLimitConfig storage cfg = _limits.configs[ + window + ]; + return (cfg.limit, cfg.amountInFlight, cfg.lastUpdated, cfg.window); + } + + function windowCountPublic() external view returns (uint256) { + return _limits.windows.length(); + } + + function hasWindowPublic(uint256 window) external view returns (bool) { + return _limits.windows.contains(window); + } +} diff --git a/contracts/testers/RedemptionVaultTest.sol b/contracts/testers/RedemptionVaultTest.sol index bdb1a890..cf50d09d 100644 --- a/contracts/testers/RedemptionVaultTest.sol +++ b/contracts/testers/RedemptionVaultTest.sol @@ -1,87 +1,110 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; -import "../RedemptionVault.sol"; - -contract RedemptionVaultTest is RedemptionVault { - bool private _overrideGetTokenRate; - uint256 private _getTokenRateValue; - - function _disableInitializers() internal override {} +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - function initializeWithoutInitializer( - address _ac, - MTokenInitParams calldata _mTokenInitParams, - ReceiversInitParams calldata _receiversInitParams, - InstantInitParams calldata _instantInitParams, - address _sanctionsList, - uint256 _variationTolerance, - uint256 _minAmount, - FiatRedeptionInitParams calldata _fiatRedemptionInitParams, - address _requestRedeemer - ) external { - __RedemptionVault_init( - _ac, - _mTokenInitParams, - _receiversInitParams, - _instantInitParams, - _sanctionsList, - _variationTolerance, - _minAmount, - _fiatRedemptionInitParams, - _requestRedeemer - ); - } - - function setOverrideGetTokenRate(bool val) external { - _overrideGetTokenRate = val; - } +import "../RedemptionVault.sol"; +import "../libraries/MidasAuthLibrary.sol"; +import {ManageableVaultTesterBase} from "./ManageableVaultTester.sol"; - function setGetTokenRateValue(uint256 val) external { - _getTokenRateValue = val; - } +abstract contract RedemptionVaultTestBase is + RedemptionVault, + ManageableVaultTesterBase +{ + function _disableInitializers() + internal + virtual + override(Initializable, ManageableVaultTesterBase) + {} function calcAndValidateRedeemTest( address user, address tokenOut, uint256 amountMTokenIn, - bool isInstant, - bool isFiat + uint256 overrideMTokenRate, + uint256 overrideTokenOutRate, + bool shouldOverrideFeePercent, + uint256 overrideFeePercent, + bool isInstant ) external returns (CalcAndValidateRedeemResult memory calcResult) { return _calcAndValidateRedeem( user, tokenOut, amountMTokenIn, - isInstant, - isFiat + overrideMTokenRate, + overrideTokenOutRate, + shouldOverrideFeePercent, + overrideFeePercent, + isInstant ); } - function convertUsdToTokenTest(uint256 amountUsd, address tokenOut) - external - returns (uint256 amountToken, uint256 tokenRate) - { - return _convertUsdToToken(amountUsd, tokenOut); + function calculateHoldbackPartRateFromAvgTest( + uint256 amountMToken, + uint256 amountMTokenInstant, + uint256 mTokenRate, + uint256 avgMTokenRate + ) external pure returns (uint256) { + return + _calculateHoldbackPartRateFromAvg( + Request({ + amountMToken: amountMToken, + amountMTokenInstant: amountMTokenInstant, + mTokenRate: mTokenRate, + tokenOut: address(0), + tokenOutRate: 0, + feePercent: 0, + recipient: address(0), + status: RequestStatus.Pending, + approvedMTokenRate: 0, + amountTokenOut: 0 + }), + avgMTokenRate + ); } - function convertMTokenToUsdTest(uint256 amountMToken) - external - returns (uint256 amountUsd, uint256 mTokenRate) - { - return _convertMTokenToUsd(amountMToken); + function convertUsdToTokenTest( + uint256 amountUsd, + address tokenOut, + uint256 overrideTokenOutRate + ) external view returns (uint256 amountToken, uint256 tokenRate) { + return _convertUsdToToken(amountUsd, tokenOut, overrideTokenOutRate); + } + + function convertMTokenToUsdTest( + uint256 amountMToken, + uint256 overrideMTokenRate + ) external view returns (uint256 amountUsd, uint256 mTokenRate) { + return _convertMTokenToUsd(amountMToken, overrideMTokenRate); } function _getTokenRate(address dataFeed, bool stable) internal view - override + virtual + override(ManageableVaultTesterBase, ManageableVault) returns (uint256) { - if (_overrideGetTokenRate) { - return _getTokenRateValue; - } + return ManageableVaultTesterBase._getTokenRate(dataFeed, stable); + } - return super._getTokenRate(dataFeed, stable); + function contractAdminRole() + public + view + virtual + override(ManageableVaultTesterBase, ManageableVault) + returns (bytes32) + { + return ManageableVault.contractAdminRole(); } } + +contract RedemptionVaultTest is RedemptionVaultTestBase { + constructor() + RedemptionVault( + keccak256("REDEMPTION_VAULT_ADMIN_ROLE"), + MidasAuthLibrary.DEFAULT_GREENLISTED_ROLE + ) + {} +} diff --git a/contracts/testers/RedemptionVaultWithAaveTest.sol b/contracts/testers/RedemptionVaultWithAaveTest.sol index 2a044421..6b7811b5 100644 --- a/contracts/testers/RedemptionVaultWithAaveTest.sol +++ b/contracts/testers/RedemptionVaultWithAaveTest.sol @@ -1,12 +1,96 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "../RedemptionVaultWithAave.sol"; +import "./RedemptionVaultTest.sol"; -contract RedemptionVaultWithAaveTest is RedemptionVaultWithAave { - function _disableInitializers() internal override {} +contract RedemptionVaultWithAaveTest is + RedemptionVaultTestBase, + RedemptionVaultWithAave +{ + constructor() + RedemptionVaultWithAave( + keccak256("REDEMPTION_VAULT_ADMIN_ROLE"), + MidasAuthLibrary.DEFAULT_GREENLISTED_ROLE + ) + {} - function checkAndRedeemAave(address token, uint256 amount) external { - _checkAndRedeemAave(token, amount); + function _disableInitializers() + internal + virtual + override(Initializable, RedemptionVaultTestBase) + { + RedemptionVaultTestBase._disableInitializers(); + } + + function checkAndRedeemAave(address token, uint256 amount) + external + returns ( + uint256 /* obtainedLiquidityBase18 */ + ) + { + uint256 tokenDecimals = _tokenDecimals(token); + uint256 balance = DecimalsCorrectionLibrary.convertToBase18( + IERC20(token).balanceOf(address(this)), + tokenDecimals + ); + uint256 amountBase18 = DecimalsCorrectionLibrary.convertToBase18( + amount, + tokenDecimals + ); + uint256 missingAmount = amountBase18 > balance + ? amountBase18 - balance + : 0; + + return + _obtainVaultLiquidity( + token, + missingAmount, + 0, + balance, + tokenDecimals + ); + } + + function _obtainVaultLiquidity( + address tokenOut, + uint256 amountTokenOutBase18, + uint256 tokenOutRate, + uint256 currentTokenOutBalanceBase18, + uint256 tokenOutDecimals + ) + internal + override(RedemptionVaultWithAave, RedemptionVault) + returns ( + uint256 /* obtainedLiquidityBase18 */ + ) + { + return + RedemptionVaultWithAave._obtainVaultLiquidity( + tokenOut, + amountTokenOutBase18, + tokenOutRate, + currentTokenOutBalanceBase18, + tokenOutDecimals + ); + } + + function _getTokenRate(address dataFeed, bool stable) + internal + view + override(RedemptionVaultTestBase, ManageableVault) + returns (uint256) + { + return RedemptionVaultTestBase._getTokenRate(dataFeed, stable); + } + + function contractAdminRole() + public + view + override(RedemptionVaultTestBase, ManageableVault) + returns (bytes32) + { + return RedemptionVaultTestBase.contractAdminRole(); } } diff --git a/contracts/testers/RedemptionVaultWithBUIDLTest.sol b/contracts/testers/RedemptionVaultWithBUIDLTest.sol deleted file mode 100644 index bf4b89fb..00000000 --- a/contracts/testers/RedemptionVaultWithBUIDLTest.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../RedemptionVaultWithBUIDL.sol"; - -contract RedemptionVaultWithBUIDLTest is RedemptionVaultWIthBUIDL { - function _disableInitializers() internal override {} -} diff --git a/contracts/testers/RedemptionVaultWithMTokenTest.sol b/contracts/testers/RedemptionVaultWithMTokenTest.sol index 118a32dc..4b7645c1 100644 --- a/contracts/testers/RedemptionVaultWithMTokenTest.sol +++ b/contracts/testers/RedemptionVaultWithMTokenTest.sol @@ -1,16 +1,100 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "../RedemptionVaultWithMToken.sol"; +import "./RedemptionVaultTest.sol"; -contract RedemptionVaultWithMTokenTest is RedemptionVaultWithMToken { - function _disableInitializers() internal override {} +contract RedemptionVaultWithMTokenTest is + RedemptionVaultTestBase, + RedemptionVaultWithMToken +{ + constructor() + RedemptionVaultWithMToken( + keccak256("REDEMPTION_VAULT_ADMIN_ROLE"), + MidasAuthLibrary.DEFAULT_GREENLISTED_ROLE + ) + {} + + function _disableInitializers() + internal + virtual + override(Initializable, RedemptionVaultTestBase) + { + RedemptionVaultTestBase._disableInitializers(); + } function checkAndRedeemMToken( address token, uint256 amount, uint256 rate - ) external { - _checkAndRedeemMToken(token, amount, rate); + ) + external + returns ( + uint256 /* obtainedLiquidityBase18 */ + ) + { + uint256 tokenDecimals = _tokenDecimals(token); + uint256 balance = DecimalsCorrectionLibrary.convertToBase18( + IERC20(token).balanceOf(address(this)), + tokenDecimals + ); + uint256 amountBase18 = DecimalsCorrectionLibrary.convertToBase18( + amount, + tokenDecimals + ); + uint256 missingAmount = amountBase18 > balance + ? amountBase18 - balance + : 0; + + return + _obtainVaultLiquidity( + token, + missingAmount, + rate, + balance, + tokenDecimals + ); + } + + function _obtainVaultLiquidity( + address token, + uint256 amountTokenOutBase18, + uint256 tokenOutRate, + uint256 currentTokenOutBalanceBase18, + uint256 tokenOutDecimals + ) + internal + override(RedemptionVaultWithMToken, RedemptionVault) + returns ( + uint256 /* obtainedLiquidityBase18 */ + ) + { + return + RedemptionVaultWithMToken._obtainVaultLiquidity( + token, + amountTokenOutBase18, + tokenOutRate, + currentTokenOutBalanceBase18, + tokenOutDecimals + ); + } + + function _getTokenRate(address dataFeed, bool stable) + internal + view + override(RedemptionVaultTestBase, ManageableVault) + returns (uint256) + { + return RedemptionVaultTestBase._getTokenRate(dataFeed, stable); + } + + function contractAdminRole() + public + view + override(RedemptionVaultTestBase, ManageableVault) + returns (bytes32) + { + return RedemptionVaultTestBase.contractAdminRole(); } } diff --git a/contracts/testers/RedemptionVaultWithMorphoTest.sol b/contracts/testers/RedemptionVaultWithMorphoTest.sol index 58f17791..f70a599d 100644 --- a/contracts/testers/RedemptionVaultWithMorphoTest.sol +++ b/contracts/testers/RedemptionVaultWithMorphoTest.sol @@ -1,12 +1,96 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "../RedemptionVaultWithMorpho.sol"; +import "./RedemptionVaultTest.sol"; -contract RedemptionVaultWithMorphoTest is RedemptionVaultWithMorpho { - function _disableInitializers() internal override {} +contract RedemptionVaultWithMorphoTest is + RedemptionVaultTestBase, + RedemptionVaultWithMorpho +{ + constructor() + RedemptionVaultWithMorpho( + keccak256("REDEMPTION_VAULT_ADMIN_ROLE"), + MidasAuthLibrary.DEFAULT_GREENLISTED_ROLE + ) + {} - function checkAndRedeemMorpho(address token, uint256 amount) external { - _checkAndRedeemMorpho(token, amount); + function _disableInitializers() + internal + virtual + override(Initializable, RedemptionVaultTestBase) + { + RedemptionVaultTestBase._disableInitializers(); + } + + function checkAndRedeemMorpho(address token, uint256 amount) + external + returns ( + uint256 /* obtainedLiquidityBase18 */ + ) + { + uint256 tokenDecimals = _tokenDecimals(token); + uint256 balance = DecimalsCorrectionLibrary.convertToBase18( + IERC20(token).balanceOf(address(this)), + tokenDecimals + ); + uint256 amountBase18 = DecimalsCorrectionLibrary.convertToBase18( + amount, + tokenDecimals + ); + uint256 missingAmount = amountBase18 > balance + ? amountBase18 - balance + : 0; + + return + _obtainVaultLiquidity( + token, + missingAmount, + 0, + balance, + tokenDecimals + ); + } + + function _obtainVaultLiquidity( + address token, + uint256 amountTokenOutBase18, + uint256 tokenOutRate, + uint256 currentTokenOutBalanceBase18, + uint256 tokenOutDecimals + ) + internal + override(RedemptionVaultWithMorpho, RedemptionVault) + returns ( + uint256 /* obtainedLiquidityBase18 */ + ) + { + return + RedemptionVaultWithMorpho._obtainVaultLiquidity( + token, + amountTokenOutBase18, + tokenOutRate, + currentTokenOutBalanceBase18, + tokenOutDecimals + ); + } + + function _getTokenRate(address dataFeed, bool stable) + internal + view + override(RedemptionVaultTestBase, ManageableVault) + returns (uint256) + { + return RedemptionVaultTestBase._getTokenRate(dataFeed, stable); + } + + function contractAdminRole() + public + view + override(RedemptionVaultTestBase, ManageableVault) + returns (bytes32) + { + return RedemptionVaultTestBase.contractAdminRole(); } } diff --git a/contracts/testers/RedemptionVaultWithSwapperTest.sol b/contracts/testers/RedemptionVaultWithSwapperTest.sol deleted file mode 100644 index 9b61cadb..00000000 --- a/contracts/testers/RedemptionVaultWithSwapperTest.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../RedemptionVaultWithSwapper.sol"; - -contract RedemptionVaultWithSwapperTest is RedemptionVaultWithSwapper { - function _disableInitializers() internal override {} -} diff --git a/contracts/testers/RedemptionVaultWithUSTBTest.sol b/contracts/testers/RedemptionVaultWithUSTBTest.sol index 2efd1dba..03f2dd33 100644 --- a/contracts/testers/RedemptionVaultWithUSTBTest.sol +++ b/contracts/testers/RedemptionVaultWithUSTBTest.sol @@ -1,12 +1,96 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "../RedemptionVaultWithUSTB.sol"; +import "./RedemptionVaultTest.sol"; -contract RedemptionVaultWithUSTBTest is RedemptionVaultWithUSTB { - function _disableInitializers() internal override {} +contract RedemptionVaultWithUSTBTest is + RedemptionVaultTestBase, + RedemptionVaultWithUSTB +{ + constructor() + RedemptionVaultWithUSTB( + keccak256("REDEMPTION_VAULT_ADMIN_ROLE"), + MidasAuthLibrary.DEFAULT_GREENLISTED_ROLE + ) + {} - function checkAndRedeemUSTB(address token, uint256 amount) external { - _checkAndRedeemUSTB(token, amount); + function _disableInitializers() + internal + virtual + override(Initializable, RedemptionVaultTestBase) + { + RedemptionVaultTestBase._disableInitializers(); + } + + function checkAndRedeemUSTB(address token, uint256 amount) + external + returns ( + uint256 /* obtainedLiquidityBase18 */ + ) + { + uint256 tokenDecimals = _tokenDecimals(token); + uint256 balance = DecimalsCorrectionLibrary.convertToBase18( + IERC20(token).balanceOf(address(this)), + tokenDecimals + ); + uint256 amountBase18 = DecimalsCorrectionLibrary.convertToBase18( + amount, + tokenDecimals + ); + uint256 missingAmount = amountBase18 > balance + ? amountBase18 - balance + : 0; + + return + _obtainVaultLiquidity( + token, + missingAmount, + 0, + balance, + tokenDecimals + ); + } + + function _obtainVaultLiquidity( + address tokenOut, + uint256 amountTokenOutBase18, + uint256 tokenOutRate, + uint256 currentTokenOutBalanceBase18, + uint256 tokenOutDecimals + ) + internal + override(RedemptionVaultWithUSTB, RedemptionVault) + returns ( + uint256 /* obtainedLiquidityBase18 */ + ) + { + return + RedemptionVaultWithUSTB._obtainVaultLiquidity( + tokenOut, + amountTokenOutBase18, + tokenOutRate, + currentTokenOutBalanceBase18, + tokenOutDecimals + ); + } + + function _getTokenRate(address dataFeed, bool stable) + internal + view + override(RedemptionVaultTestBase, ManageableVault) + returns (uint256) + { + return RedemptionVaultTestBase._getTokenRate(dataFeed, stable); + } + + function contractAdminRole() + public + view + override(RedemptionVaultTestBase, ManageableVault) + returns (bytes32) + { + return RedemptionVaultTestBase.contractAdminRole(); } } diff --git a/contracts/testers/WithMidasAccessControlTester.sol b/contracts/testers/WithMidasAccessControlTester.sol index 1105d61b..490be609 100644 --- a/contracts/testers/WithMidasAccessControlTester.sol +++ b/contracts/testers/WithMidasAccessControlTester.sol @@ -1,9 +1,25 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../access/WithMidasAccessControl.sol"; contract WithMidasAccessControlTester is WithMidasAccessControl { + bytes32 private _contractAdminRoleOverride; + + /** + * @notice copy of `RolePreflightSucceeded` with a different name for testing + */ + error WrongRolePreflightSucceeded( + bytes32 role, + uint32 overrideDelay, + bool roleIsFunctionOperator, + bool validateFunctionRole + ); + + function setContractAdminRole(bytes32 role) external { + _contractAdminRoleOverride = role; + } + function initialize(address _accessControl) external initializer { __WithMidasAccessControl_init(_accessControl); } @@ -12,23 +28,39 @@ contract WithMidasAccessControlTester is WithMidasAccessControl { __WithMidasAccessControl_init(_accessControl); } - function grantRoleTester(bytes32 role, address account) external { - accessControl.grantRole(role, account); - } - - function revokeRoleTester(bytes32 role, address account) external { - accessControl.revokeRole(role, account); - } - - function withOnlyRole(bytes32 role, address account) + function withOnlyRole(bytes32 role, bool validateFunctionRole) external - onlyRole(role, account) + onlyRole(role, validateFunctionRole) {} - function withOnlyNotRole(bytes32 role, address account) + function withOnlyRoleNoTimelock(bytes32 role, bool validateFunctionRole) external - onlyNotRole(role, account) + onlyRoleNoTimelock(role, validateFunctionRole) {} + function withOnlyContractAdmin() external onlyContractAdmin {} + + function withUnprotected() external {} + + function withWrongRolePreflight( + bytes32 role, + uint32 overrideDelay, + bool roleIsFunctionOperator, + bool validateFunctionRole + ) external pure { + revert WrongRolePreflightSucceeded( + role, + overrideDelay, + roleIsFunctionOperator, + validateFunctionRole + ); + } + + function contractAdminRole() public view override returns (bytes32) { + return _contractAdminRoleOverride; + } + function _disableInitializers() internal override {} + + function _onlyProxyAdmin() internal view override {} } diff --git a/contracts/testers/WithSanctionsListTester.sol b/contracts/testers/WithSanctionsListTester.sol index 45052ef1..9cb2eed0 100644 --- a/contracts/testers/WithSanctionsListTester.sol +++ b/contracts/testers/WithSanctionsListTester.sol @@ -1,22 +1,15 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../abstract/WithSanctionsList.sol"; -// TODO: add natspec contract WithSanctionsListTester is WithSanctionsList { function initialize(address _accessControl, address _sanctionsList) external initializer { - __WithSanctionsList_init(_accessControl, _sanctionsList); - } - - function initializeWithoutInitializer( - address _accessControl, - address _sanctionsList - ) external { - __WithSanctionsList_init(_accessControl, _sanctionsList); + __WithMidasAccessControl_init(_accessControl); + __WithSanctionsList_init_unchained(_sanctionsList); } function initializeUnchainedWithoutInitializer(address _sanctionsList) @@ -30,9 +23,15 @@ contract WithSanctionsListTester is WithSanctionsList { onlyNotSanctioned(user) {} - function sanctionsListAdminRole() public pure override returns (bytes32) { + function sanctionsListAdminRole() public pure returns (bytes32) { return keccak256("TESTER_SANCTIONS_LIST_ADMIN_ROLE"); } + function contractAdminRole() public pure override returns (bytes32) { + return _DEFAULT_ADMIN_ROLE; + } + function _disableInitializers() internal override {} + + function _onlyProxyAdmin() internal view override {} } diff --git a/contracts/testers/mTBILLTest.sol b/contracts/testers/mTBILLTest.sol deleted file mode 100644 index 99fcc8c5..00000000 --- a/contracts/testers/mTBILLTest.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../products/mTBILL/mTBILL.sol"; - -//solhint-disable contract-name-camelcase -contract mTBILLTest is mTBILL { - function _disableInitializers() internal override {} -} diff --git a/contracts/testers/mTokenPermissionedTest.sol b/contracts/testers/mTokenPermissionedTest.sol index 9abc7d0e..c8a5e652 100644 --- a/contracts/testers/mTokenPermissionedTest.sol +++ b/contracts/testers/mTokenPermissionedTest.sol @@ -1,46 +1,20 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; import "../mTokenPermissioned.sol"; //solhint-disable contract-name-camelcase contract mTokenPermissionedTest is mTokenPermissioned { - bytes32 public constant M_TOKEN_TEST_MINT_OPERATOR_ROLE = - keccak256("M_TOKEN_TEST_MINT_OPERATOR_ROLE"); - - bytes32 public constant M_TOKEN_TEST_BURN_OPERATOR_ROLE = - keccak256("M_TOKEN_TEST_BURN_OPERATOR_ROLE"); - - bytes32 public constant M_TOKEN_TEST_PAUSE_OPERATOR_ROLE = - keccak256("M_TOKEN_TEST_PAUSE_OPERATOR_ROLE"); - - bytes32 public constant M_TOKEN_TEST_GREENLISTED_ROLE = - keccak256("M_TOKEN_TEST_GREENLISTED_ROLE"); + constructor() + mTokenPermissioned( + keccak256("M_TOKEN_MANAGER_ROLE"), + keccak256("M_TOKEN_TEST_MINT_OPERATOR_ROLE"), + keccak256("M_TOKEN_TEST_BURN_OPERATOR_ROLE"), + keccak256("M_TOKEN_TEST_GREENLISTED_ROLE") + ) + {} function _disableInitializers() internal override {} - function _getNameSymbol() - internal - pure - override - returns (string memory, string memory) - { - return ("mTokenPermissionedTest", "mTokenPermissionedTest"); - } - - function _minterRole() internal pure override returns (bytes32) { - return M_TOKEN_TEST_MINT_OPERATOR_ROLE; - } - - function _burnerRole() internal pure override returns (bytes32) { - return M_TOKEN_TEST_BURN_OPERATOR_ROLE; - } - - function _pauserRole() internal pure override returns (bytes32) { - return M_TOKEN_TEST_PAUSE_OPERATOR_ROLE; - } - - function _greenlistedRole() internal pure override returns (bytes32) { - return M_TOKEN_TEST_GREENLISTED_ROLE; - } + function _onlyProxyAdmin() internal view override {} } diff --git a/contracts/testers/mTokenTest.sol b/contracts/testers/mTokenTest.sol new file mode 100644 index 00000000..ca8e0c81 --- /dev/null +++ b/contracts/testers/mTokenTest.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.34; + +import "../mToken.sol"; + +//solhint-disable contract-name-camelcase +contract mTokenTest is mToken { + constructor( + bytes32 _managerRole, + bytes32 _mintOperatorRole, + bytes32 _burnOperatorRole + ) mToken(_managerRole, _mintOperatorRole, _burnOperatorRole) {} + + function _disableInitializers() internal override {} + + function _onlyProxyAdmin() internal view override {} +} diff --git a/docgen/index.md b/docgen/index.md index dda34bdf..a3bde4a7 100644 --- a/docgen/index.md +++ b/docgen/index.md @@ -1,160 +1,187 @@ # Solidity API -## RedemptionVault +## DepositVault -Smart contract that handles mTBILL redemptions +Smart contract that handles mToken minting -### CalcAndValidateRedeemResult +### CalcAndValidateDepositResult + +return data of _calcAndValidateDeposit +packed into a struct to avoid stack too deep errors ```solidity -struct CalcAndValidateRedeemResult { - uint256 feeAmount; - uint256 amountMTokenWithoutFee; +struct CalcAndValidateDepositResult { + uint256 tokenAmountInUsd; + uint256 feeTokenAmount; + uint256 amountTokenWithoutFee; + uint256 mintAmount; + uint256 tokenInRate; + uint256 tokenOutRate; + uint256 tokenDecimals; } ``` -### minFiatRedeemAmount +### minMTokenAmountForFirstDeposit ```solidity -uint256 minFiatRedeemAmount +uint256 minMTokenAmountForFirstDeposit ``` -min amount for fiat requests +minimal USD amount for first user`s deposit -### fiatAdditionalFee +### mintRequests ```solidity -uint256 fiatAdditionalFee +mapping(uint256 => struct RequestV2) mintRequests ``` -fee percent for fiat requests - -### fiatFlatFee +request data storage -```solidity -uint256 fiatFlatFee -``` +_mapping, requestId => request data_ -static fee in mToken for fiat requests - -### redeemRequests +### totalMinted ```solidity -mapping(uint256 => struct Request) redeemRequests +mapping(address => uint256) totalMinted ``` -mapping, requestId to request data +_how much mTokens were minted by the depositor +depositor address => amount minted_ -### requestRedeemer +### maxSupplyCap ```solidity -address requestRedeemer +uint256 maxSupplyCap ``` -address is designated for standard redemptions, allowing tokens to be pulled from this address +max supply cap value in mToken + +_if after the deposit, mToken.totalSupply() > maxSupplyCap, +the tx will be reverted_ ### initialize ```solidity -function initialize(address _ac, struct MTokenInitParams _mTokenInitParams, struct ReceiversInitParams _receiversInitParams, struct InstantInitParams _instantInitParams, address _sanctionsList, uint256 _variationTolerance, uint256 _minAmount, struct FiatRedeptionInitParams _fiatRedemptionInitParams, address _requestRedeemer) external +function initialize(struct CommonVaultInitParams _commonVaultInitParams, struct CommonVaultV2InitParams _commonVaultV2InitParams, uint256 _minMTokenAmountForFirstDeposit, uint256 _maxSupplyCap) public ``` upgradeable pattern contract`s initializer +_Calls all versioned initializers (V1, V2, ...) in chronological order. +This ensures that every deployment, whether fresh or upgraded, ends up +initialized to the latest contract state without breaking the +initializer/reinitializer versioning rules._ + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| _ac | address | address of MidasAccessControll contract | -| _mTokenInitParams | struct MTokenInitParams | init params for mToken | -| _receiversInitParams | struct ReceiversInitParams | init params for receivers | -| _instantInitParams | struct InstantInitParams | init params for instant operations | -| _sanctionsList | address | address of sanctionsList contract | -| _variationTolerance | uint256 | percent of prices diviation 1% = 100 | -| _minAmount | uint256 | basic min amount for operations | -| _fiatRedemptionInitParams | struct FiatRedeptionInitParams | params fiatAdditionalFee, fiatFlatFee, minFiatRedeemAmount | -| _requestRedeemer | address | address is designated for standard redemptions, allowing tokens to be pulled from this address | +| _commonVaultInitParams | struct CommonVaultInitParams | init params for common vault | +| _commonVaultV2InitParams | struct CommonVaultV2InitParams | init params for common vault v2 | +| _minMTokenAmountForFirstDeposit | uint256 | min amount for first deposit in mToken | +| _maxSupplyCap | uint256 | max supply cap for mToken | -### __RedemptionVault_init +### initializeV1 ```solidity -function __RedemptionVault_init(address _ac, struct MTokenInitParams _mTokenInitParams, struct ReceiversInitParams _receiversInitParams, struct InstantInitParams _instantInitParams, address _sanctionsList, uint256 _variationTolerance, uint256 _minAmount, struct FiatRedeptionInitParams _fiatRedemptionInitParams, address _requestRedeemer) internal +function initializeV1(struct CommonVaultInitParams _commonVaultInitParams, uint256 _minMTokenAmountForFirstDeposit) public ``` -### redeemInstant +v1 initializer + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _commonVaultInitParams | struct CommonVaultInitParams | init params for common vault | +| _minMTokenAmountForFirstDeposit | uint256 | min amount for first deposit in mToken | + +### initializeV2 ```solidity -function redeemInstant(address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount) external +function initializeV2(uint256 _maxSupplyCap) public ``` -redeem mToken to tokenOut if daily limit and allowance not exceeded -Burns mTBILL from the user. -Transfers fee in mToken to feeReceiver -Transfers tokenOut to user. +v2 initializer #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tokenOut | address | stable coin token address to redeem to | -| amountMTokenIn | uint256 | amount of mTBILL to redeem (decimals 18) | -| minReceiveAmount | uint256 | minimum expected amount of tokenOut to receive (decimals 18) | +| _maxSupplyCap | uint256 | max supply cap for mToken | -### redeemInstant +### initializeV3 ```solidity -function redeemInstant(address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount, address recipient) external +function initializeV3(struct CommonVaultV2InitParams _commonVaultV2InitParams) public ``` -Does the same as `redeemInstant` but allows specifying a custom tokensReceiver address. +v2 initializer #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tokenOut | address | stable coin token address to redeem to | -| amountMTokenIn | uint256 | amount of mTBILL to redeem (decimals 18) | -| minReceiveAmount | uint256 | minimum expected amount of tokenOut to receive (decimals 18) | -| recipient | address | address that receives tokens | +| _commonVaultV2InitParams | struct CommonVaultV2InitParams | init params for common vault v2 | -### redeemRequest +### depositInstant ```solidity -function redeemRequest(address tokenOut, uint256 amountMTokenIn) external returns (uint256) +function depositInstant(address tokenIn, uint256 amountToken, uint256 minReceiveAmount, bytes32 referrerId) external ``` -creating redeem request if tokenOut not fiat -Transfers amount in mToken to contract -Transfers fee in mToken to feeReceiver +depositing proccess with auto mint if +account fit daily limit and token allowance. +Transfers token from the user. +Transfers fee in tokenIn to feeReceiver. +Mints mToken to user. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tokenOut | address | stable coin token address to redeem to | -| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | +| tokenIn | address | address of tokenIn | +| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | +| minReceiveAmount | uint256 | minimum expected amount of mToken to receive (decimals 18) | +| referrerId | bytes32 | referrer id | -#### Return Values +### depositInstant + +```solidity +function depositInstant(address tokenIn, uint256 amountToken, uint256 minReceiveAmount, bytes32 referrerId, address recipient) external +``` + +Does the same as original `depositInstant` but allows specifying a custom tokensReceiver address. + +#### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | uint256 | request id | +| tokenIn | address | address of tokenIn | +| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | +| minReceiveAmount | uint256 | minimum expected amount of mToken to receive (decimals 18) | +| referrerId | bytes32 | referrer id | +| recipient | address | | -### redeemRequest +### depositRequest ```solidity -function redeemRequest(address tokenOut, uint256 amountMTokenIn, address recipient) external returns (uint256) +function depositRequest(address tokenIn, uint256 amountToken, bytes32 referrerId) external returns (uint256) ``` -Does the same as `redeemRequest` but allows specifying a custom tokensReceiver address. +depositing proccess with mint request creating if +account fit token allowance. +Transfers token from the user. +Transfers fee in tokenIn to feeReceiver. +Creates mint request. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tokenOut | address | stable coin token address to redeem to | -| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | -| recipient | address | address that receives tokens | +| tokenIn | address | address of tokenIn | +| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | +| referrerId | bytes32 | referrer id | #### Return Values @@ -162,21 +189,22 @@ Does the same as `redeemRequest` but allows specifying a custom tokensReceiver a | ---- | ---- | ----------- | | [0] | uint256 | request id | -### redeemFiatRequest +### depositRequest ```solidity -function redeemFiatRequest(uint256 amountMTokenIn) external returns (uint256) +function depositRequest(address tokenIn, uint256 amountToken, bytes32 referrerId, address recipient) external returns (uint256) ``` -creating redeem request if tokenOut is fiat -Transfers amount in mToken to contract -Transfers fee in mToken to feeReceiver +Does the same as original `depositRequest` but allows specifying a custom tokensReceiver address. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | +| tokenIn | address | address of tokenIn | +| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | +| referrerId | bytes32 | referrer id | +| recipient | address | address that receives the mTokens | #### Return Values @@ -184,1555 +212,1599 @@ Transfers fee in mToken to feeReceiver | ---- | ---- | ----------- | | [0] | uint256 | request id | -### approveRequest +### depositRequest ```solidity -function approveRequest(uint256 requestId, uint256 newMTokenRate) external +function depositRequest(address tokenIn, uint256 amountToken, bytes32 referrerId, address recipientRequest, uint256 instantShare, uint256 minReceiveAmountInstantShare, address recipientInstant) external returns (uint256) ``` -approving redeem request if not exceed tokenOut allowance -Burns amount mToken from contract -Transfers tokenOut to user -Sets flag Processed +Instantly deposits `instantShare` amount of `amountMTokenIn` and creates a request for the remaining amount. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| requestId | uint256 | request id | -| newMTokenRate | uint256 | new mToken rate inputted by vault admin | +| tokenIn | address | address of tokenIn | +| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | +| referrerId | bytes32 | referrer id | +| recipientRequest | address | address that receives the mTokens for the request part | +| instantShare | uint256 | % amount of `amountToken` that will be deposited instantly | +| minReceiveAmountInstantShare | uint256 | min receive amount for the instant share | +| recipientInstant | address | address that receives the mTokens for the instant part | -### safeApproveRequest +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | request id | + +### safeBulkApproveRequestAtSavedRate ```solidity -function safeApproveRequest(uint256 requestId, uint256 newMTokenRate) external +function safeBulkApproveRequestAtSavedRate(uint256[] requestIds) external ``` -approving request if inputted token rate fit price diviation percent -Burns amount mToken from contract -Transfers tokenOut to user -Sets flag Processed +approving requests from the `requestIds` array +with the mToken rate from the request. +Does same validation as `safeApproveRequest`. +Mints mToken to request users. +Sets request flags to Processed. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| requestId | uint256 | request id | -| newMTokenRate | uint256 | new mToken rate inputted by vault admin | +| requestIds | uint256[] | request ids array | -### rejectRequest +### safeBulkApproveRequest ```solidity -function rejectRequest(uint256 requestId) external +function safeBulkApproveRequest(uint256[] requestIds) external ``` -rejecting request -Sets request flag to Canceled. +approving requests from the `requestIds` array +with the current mToken rate. +Does same validation as `safeApproveRequest`. +Mints mToken to request users. +Sets request flags to Processed. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| requestId | uint256 | request id | +| requestIds | uint256[] | request ids array | -### setMinFiatRedeemAmount +### safeBulkApproveRequestAvgRate ```solidity -function setMinFiatRedeemAmount(uint256 newValue) external +function safeBulkApproveRequestAvgRate(uint256[] requestIds) external ``` -set new min amount for fiat requests +approving requests from the `requestIds` array +with the current mToken rate. +Does same validation as `safeApproveRequestAvgRate`. +Mints mToken to request users. +Sets request flags to Processed. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| newValue | uint256 | new min amount | +| requestIds | uint256[] | request ids array | -### setFiatFlatFee +### safeBulkApproveRequest ```solidity -function setFiatFlatFee(uint256 feeInMToken) external +function safeBulkApproveRequest(uint256[] requestIds, uint256 newOutRate) external ``` -set fee amount in mToken for fiat requests +approving requests from the `requestIds` array using the `newOutRate`. +Does same validation as `safeApproveRequest`. +Mints mToken to request users. +Sets request flags to Processed. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| feeInMToken | uint256 | fee amount in mToken | +| requestIds | uint256[] | request ids array | +| newOutRate | uint256 | new mToken rate inputted by vault admin | -### setFiatAdditionalFee +### safeBulkApproveRequestAvgRate ```solidity -function setFiatAdditionalFee(uint256 newFee) external +function safeBulkApproveRequestAvgRate(uint256[] requestIds, uint256 avgMTokenRate) external ``` -set new fee percent for fiat requests +approving requests from the `requestIds` array using the `newOutRate`. +Does same validation as `safeApproveRequestAvgRate`. +Mints mToken to request users. +Sets request flags to Processed. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| newFee | uint256 | new fee percent 1% = 100 | +| requestIds | uint256[] | request ids array | +| avgMTokenRate | uint256 | avg mToken rate inputted by vault admin | -### setRequestRedeemer +### safeApproveRequest ```solidity -function setRequestRedeemer(address redeemer) external +function safeApproveRequest(uint256 requestId, uint256 newOutRate) external ``` -set address which is designated for standard redemptions, allowing tokens to be pulled from this address +approving request if inputted token rate fit price deviation percent +Mints mToken to user. +Sets request flag to Processed. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| redeemer | address | new address of request redeemer | +| requestId | uint256 | request id | +| newOutRate | uint256 | mToken rate inputted by vault admin | -### vaultRole +### safeApproveRequestAvgRate ```solidity -function vaultRole() public pure virtual returns (bytes32) +function safeApproveRequestAvgRate(uint256 requestId, uint256 avgMTokenRate) external ``` -AC role of vault administrator +approving request if inputted token rate fit price deviation percent +Mints mToken to user. +Sets request flag to Processed. -#### Return Values +#### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | bytes32 | role bytes32 role | +| requestId | uint256 | request id | +| avgMTokenRate | uint256 | avg mToken rate inputted by vault admin | -### _approveRequest +### approveRequest ```solidity -function _approveRequest(uint256 requestId, uint256 newMTokenRate, bool isSafe) internal +function approveRequest(uint256 requestId, uint256 newOutRate) external ``` -validates approve -burns amount from contract -transfer tokenOut to user if not fiat -sets flag Processed +approving request without price deviation check +Mints mToken to user. +Sets request flag to Processed. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | | requestId | uint256 | request id | -| newMTokenRate | uint256 | new mToken rate | -| isSafe | bool | new mToken rate | +| newOutRate | uint256 | mToken rate inputted by vault admin | -### _validateRequest +### approveRequestAvgRate ```solidity -function _validateRequest(address sender, enum RequestStatus status) internal pure +function approveRequestAvgRate(uint256 requestId, uint256 avgMTokenRate) external ``` -validates request -if exist -if not processed +approving request without price deviation check +Mints mToken to user. +Sets request flag to Processed. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| sender | address | sender address | -| status | enum RequestStatus | request status | +| requestId | uint256 | request id | +| avgMTokenRate | uint256 | avg mToken rate inputted by vault admin | -### _redeemInstant +### rejectRequest ```solidity -function _redeemInstant(address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount, address recipient) internal virtual returns (struct RedemptionVault.CalcAndValidateRedeemResult calcResult, uint256 amountTokenOutWithoutFee) +function rejectRequest(uint256 requestId) external ``` -_internal redeem instant logic_ +rejecting request +Sets request flag to Canceled. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tokenOut | address | tokenOut address | -| amountMTokenIn | uint256 | amount of mToken (decimals 18) | -| minReceiveAmount | uint256 | min amount of tokenOut to receive (decimals 18) | -| recipient | address | recipient address | +| requestId | uint256 | request id | -#### Return Values +### setMinMTokenAmountForFirstDeposit + +```solidity +function setMinMTokenAmountForFirstDeposit(uint256 newValue) external +``` + +sets new minimal amount to deposit in EUR. +can be called only from vault`s admin + +#### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| calcResult | struct RedemptionVault.CalcAndValidateRedeemResult | calculated redeem result | -| amountTokenOutWithoutFee | uint256 | amount of tokenOut without fee | +| newValue | uint256 | new min. deposit value | -### _redeemRequest +### setMaxSupplyCap ```solidity -function _redeemRequest(address tokenOut, uint256 amountMTokenIn, bool isFiat, address recipient) internal returns (uint256 requestId, struct RedemptionVault.CalcAndValidateRedeemResult calcResult) +function setMaxSupplyCap(uint256 newValue) external ``` -internal redeem request logic +sets new max supply cap value +can be called only from vault`s admin #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tokenOut | address | tokenOut address | -| amountMTokenIn | uint256 | amount of mToken (decimals 18) | -| isFiat | bool | | -| recipient | address | | +| newValue | uint256 | new max supply cap value | + +### vaultRole + +```solidity +function vaultRole() public pure virtual returns (bytes32) +``` + +AC role of vault administrator #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| requestId | uint256 | request id | -| calcResult | struct RedemptionVault.CalcAndValidateRedeemResult | calc result | +| [0] | bytes32 | role bytes32 role | -### _convertUsdToToken +### _safeBulkApproveRequest ```solidity -function _convertUsdToToken(uint256 amountUsd, address tokenOut) internal view returns (uint256 amountToken, uint256 tokenRate) +function _safeBulkApproveRequest(uint256[] requestIds, uint256 newOutRate, bool isAvgRate) internal ``` -_calculates tokenOut amount from USD amount_ +_internal function to approve requests_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| amountUsd | uint256 | amount of USD (decimals 18) | -| tokenOut | address | tokenOut address | - -#### Return Values - -| Name | Type | Description | -| ---- | ---- | ----------- | -| amountToken | uint256 | converted USD to tokenOut | -| tokenRate | uint256 | conversion rate | +| requestIds | uint256[] | request ids | +| newOutRate | uint256 | new out rate | +| isAvgRate | bool | if true, newOutRate is avg rate | -### _convertMTokenToUsd +### _depositInstant ```solidity -function _convertMTokenToUsd(uint256 amountMToken) internal view returns (uint256 amountUsd, uint256 mTokenRate) +function _depositInstant(address tokenIn, uint256 amountToken, uint256 minReceiveAmount, address recipient) internal virtual returns (struct DepositVault.CalcAndValidateDepositResult result) ``` -_calculates USD amount from mToken amount_ +_internal deposit instant logic_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| amountMToken | uint256 | amount of mToken (decimals 18) | +| tokenIn | address | tokenIn address | +| amountToken | uint256 | amount of tokenIn (decimals 18) | +| minReceiveAmount | uint256 | min amount of mToken to receive (decimals 18) | +| recipient | address | recipient address | #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| amountUsd | uint256 | converted amount to USD | -| mTokenRate | uint256 | conversion rate | +| result | struct DepositVault.CalcAndValidateDepositResult | calculated deposit result | -### _calcAndValidateRedeem +### _instantTransferTokensToTokensReceiver ```solidity -function _calcAndValidateRedeem(address user, address tokenOut, uint256 amountMTokenIn, bool isInstant, bool isFiat) internal view returns (struct RedemptionVault.CalcAndValidateRedeemResult result) +function _instantTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal virtual ``` -_validate redeem and calculate fee_ +_internal transfer tokens to tokens receiver (instant deposits)_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| user | address | user address | -| tokenOut | address | tokenOut address | -| amountMTokenIn | uint256 | mToken amount (decimals 18) | -| isInstant | bool | is instant operation | -| isFiat | bool | is fiat operation | - -#### Return Values - -| Name | Type | Description | -| ---- | ---- | ----------- | -| result | struct RedemptionVault.CalcAndValidateRedeemResult | calc result | - -## RedemptionVaultWIthBUIDL - -Smart contract that handles mTBILL redemptions +| tokenIn | address | tokenIn address | +| amountToken | uint256 | amount of tokenIn (decimals 18) | +| tokensDecimals | uint256 | tokens decimals | -### minBuidlToRedeem +### _requestTransferTokensToTokensReceiver ```solidity -uint256 minBuidlToRedeem +function _requestTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal virtual ``` -minimum amount of BUIDL to redeem. Will redeem at least this amount of BUIDL. +_internal transfer tokens to tokens receiver (deposit requests)_ -### minBuidlBalance +#### Parameters -```solidity -uint256 minBuidlBalance -``` +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenIn | address | tokenIn address | +| amountToken | uint256 | amount of tokenIn (decimals 18) | +| tokensDecimals | uint256 | tokens decimals | -### buidlRedemption +### _validateRequest ```solidity -contract IRedemption buidlRedemption +function _validateRequest(address validateAddress, enum RequestStatus status) internal pure ``` -### SetMinBuidlToRedeem +validates request +if exist +if not processed + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| validateAddress | address | address to check if not zero | +| status | enum RequestStatus | request status | + +### _calcAndValidateDeposit ```solidity -event SetMinBuidlToRedeem(uint256 minBuidlToRedeem, address sender) +function _calcAndValidateDeposit(address user, address tokenIn, uint256 amountToken, bool isInstant) internal returns (struct DepositVault.CalcAndValidateDepositResult result) ``` +_validate deposit and calculate mint amount_ + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| minBuidlToRedeem | uint256 | new min amount of BUIDL to redeem | -| sender | address | address who set new min amount of BUIDL to redeem | +| user | address | user address | +| tokenIn | address | tokenIn address | +| amountToken | uint256 | tokenIn amount (decimals 18) | +| isInstant | bool | is instant operation | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| result | struct DepositVault.CalcAndValidateDepositResult | calculated deposit result | -### SetMinBuidlBalance +### _validateMinAmount ```solidity -event SetMinBuidlBalance(uint256 minBuidlBalance, address sender) +function _validateMinAmount(address user, uint256 amountMTokenWithoutFee) internal view ``` +_validates that inputted USD amount >= minAmountToDepositInUsd() +and amount >= minAmount()_ + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| minBuidlBalance | uint256 | new `minBuidlBalance` value | -| sender | address | address who set new `minBuidlBalance` | +| user | address | user address | +| amountMTokenWithoutFee | uint256 | amount of mToken without fee (decimals 18) | -### initialize +### _validateMaxSupplyCap ```solidity -function initialize(address _ac, struct MTokenInitParams _mTokenInitParams, struct ReceiversInitParams _receiversInitParams, struct InstantInitParams _instantInitParams, address _sanctionsList, uint256 _variationTolerance, uint256 _minAmount, struct FiatRedeptionInitParams _fiatRedemptionInitParams, address _requestRedeemer, address _buidlRedemption, uint256 _minBuidlToRedeem, uint256 _minBuidlBalance) external +function _validateMaxSupplyCap(bool revertOnError) internal view returns (bool) ``` -upgradeable pattern contract`s initializer +_validates that mToken.totalSupply() <= maxSupplyCap_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| _ac | address | address of MidasAccessControll contract | -| _mTokenInitParams | struct MTokenInitParams | init params for mToken | -| _receiversInitParams | struct ReceiversInitParams | init params for receivers | -| _instantInitParams | struct InstantInitParams | init params for instant operations | -| _sanctionsList | address | address of sanctionsList contract | -| _variationTolerance | uint256 | percent of prices diviation 1% = 100 | -| _minAmount | uint256 | basic min amount for operations | -| _fiatRedemptionInitParams | struct FiatRedeptionInitParams | params fiatAdditionalFee, fiatFlatFee, minFiatRedeemAmount | -| _requestRedeemer | address | address is designated for standard redemptions, allowing tokens to be pulled from this address | -| _buidlRedemption | address | BUIDL redemption contract address | -| _minBuidlToRedeem | uint256 | | -| _minBuidlBalance | uint256 | | +| revertOnError | bool | if true, will revert if supply is exceeded if false, will return false if supply is exceeded without reverting | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | true if supply is valid, false otherwise | -### setMinBuidlToRedeem +### _validateMaxSupplyCap ```solidity -function setMinBuidlToRedeem(uint256 _minBuidlToRedeem) external +function _validateMaxSupplyCap(uint256 mintAmount, bool revertOnError) internal view returns (bool) ``` -set min amount of BUIDL to redeem. +_validates that mToken.totalSupply() <= maxSupplyCap_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| _minBuidlToRedeem | uint256 | min amount of BUIDL to redeem | +| mintAmount | uint256 | amount of mToken to mint | +| revertOnError | bool | if true, will revert if supply is exceeded if false, will return false if supply is exceeded without reverting | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | true if supply is valid, false otherwise | -### setMinBuidlBalance +### _convertTokenToUsd ```solidity -function setMinBuidlBalance(uint256 _minBuidlBalance) external +function _convertTokenToUsd(address tokenIn, uint256 amount) internal view virtual returns (uint256 amountInUsd, uint256 rate) ``` -set new `minBuidlBalance` value. +_calculates USD amount from tokenIn amount_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| _minBuidlBalance | uint256 | new `minBuidlBalance` value | +| tokenIn | address | tokenIn address | +| amount | uint256 | amount of tokenIn (decimals 18) | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| amountInUsd | uint256 | converted amount to USD | +| rate | uint256 | conversion rate | -### _redeemInstant +### _convertUsdToMToken ```solidity -function _redeemInstant(address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount, address recipient) internal returns (struct RedemptionVault.CalcAndValidateRedeemResult calcResult, uint256 amountTokenOutWithoutFee) +function _convertUsdToMToken(uint256 amountUsd) internal view virtual returns (uint256 amountMToken, uint256 mTokenRate) ``` -_redeem mToken to USDC if daily limit and allowance not exceeded -If contract don't have enough USDC, BUIDL redemption flow will be triggered -Burns mToken from the user. -Transfers fee in mToken to feeReceiver -Transfers tokenOut to user._ +_calculates mToken amount from USD amount_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tokenOut | address | token out address, always ignore | -| amountMTokenIn | uint256 | amount of mToken to redeem | -| minReceiveAmount | uint256 | minimum expected amount of tokenOut to receive (decimals 18) | -| recipient | address | | +| amountUsd | uint256 | amount of USD (decimals 18) | -### _checkAndRedeemBUIDL +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| amountMToken | uint256 | converted USD to mToken | +| mTokenRate | uint256 | conversion rate | + +### _calculateHoldbackPartRateFromAvg ```solidity -function _checkAndRedeemBUIDL(address tokenOut, uint256 amountTokenOut) internal +function _calculateHoldbackPartRateFromAvg(struct RequestV2 request, uint256 avgMTokenRate) internal pure returns (uint256) ``` -Check if contract have enough USDC balance for redeem -if don't have trigger BUIDL redemption flow +_calculates holdback part rate from avg rate_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tokenOut | address | tokenOut address | -| amountTokenOut | uint256 | amount of tokenOut | +| request | struct RequestV2 | request | +| avgMTokenRate | uint256 | avg mToken rate | -## RedemptionVaultWithSwapper +#### Return Values -Smart contract that handles mToken redemption. -In case of insufficient liquidity it uses a RV from a different -Midas product to fulfill instant redemption. +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | holdback part rate | -_mToken1 - is a main mToken of this vault -mToken2 - is a token of a second vault that is triggered when -current vault don`t have enough liquidity_ +## DepositVaultWithAave -### mTbillRedemptionVault +Smart contract that handles mToken minting and invests +proceeds into Aave V3 Pool + +_If `aaveDepositsEnabled` is false, regular deposit flow is used_ + +### aavePools ```solidity -contract IRedemptionVault mTbillRedemptionVault +mapping(address => contract IAaveV3Pool) aavePools ``` -mToken1 redemption vault +mapping payment token to Aave V3 Pool -_The naming was not altered to maintain -compatibility with the currently deployed contracts._ - -### liquidityProvider +### aaveDepositsEnabled ```solidity -address liquidityProvider +bool aaveDepositsEnabled ``` -### initialize +Whether Aave auto-invest deposits are enabled + +_if false, regular deposit flow will be used_ + +### autoInvestFallbackEnabled ```solidity -function initialize(address _ac, struct MTokenInitParams _mTokenInitParams, struct ReceiversInitParams _receiversInitParams, struct InstantInitParams _instantInitParams, address _sanctionsList, uint256 _variationTolerance, uint256 _minAmount, struct FiatRedeptionInitParams _fiatRedemptionInitParams, address _requestRedeemer, address _mTbillRedemptionVault, address _liquidityProvider) external +bool autoInvestFallbackEnabled ``` -upgradeable pattern contract`s initializer - -#### Parameters +Whether to fall back to raw token transfer on auto-invest failure -| Name | Type | Description | -| ---- | ---- | ----------- | -| _ac | address | address of MidasAccessControll contract | -| _mTokenInitParams | struct MTokenInitParams | init params for mToken1 | -| _receiversInitParams | struct ReceiversInitParams | init params for receivers | -| _instantInitParams | struct InstantInitParams | init params for instant operations | -| _sanctionsList | address | address of sanctionsList contract | -| _variationTolerance | uint256 | percent of prices diviation 1% = 100 | -| _minAmount | uint256 | basic min amount for operations | -| _fiatRedemptionInitParams | struct FiatRedeptionInitParams | params fiatAdditionalFee, fiatFlatFee, minFiatRedeemAmount | -| _requestRedeemer | address | address is designated for standard redemptions, allowing tokens to be pulled from this address | -| _mTbillRedemptionVault | address | mToken2 redemptionVault address | -| _liquidityProvider | address | liquidity provider for pull mToken2 | +_if false, the transaction will revert when auto-invest fails_ -### _redeemInstant +### SetAavePool ```solidity -function _redeemInstant(address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount, address recipient) internal returns (struct RedemptionVault.CalcAndValidateRedeemResult calcResult, uint256 amountTokenOutWithoutFee) +event SetAavePool(address caller, address token, address pool) ``` -_redeem mToken1 to tokenOut if daily limit and allowance not exceeded -If contract don't have enough tokenOut, mToken1 will swap to mToken2 and redeem on mToken2 vault -Burns mToken1 from the user, if swap need mToken1 just tranfers to contract. -Transfers fee in mToken1 to feeReceiver -Transfers tokenOut to user._ +Emitted when an Aave V3 Pool is configured for a payment token #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tokenOut | address | token out address | -| amountMTokenIn | uint256 | amount of mToken1 to redeem | -| minReceiveAmount | uint256 | minimum expected amount of tokenOut to receive (decimals 18) | -| recipient | address | | +| caller | address | address of the caller | +| token | address | payment token address | +| pool | address | Aave V3 Pool address | -### setLiquidityProvider +### RemoveAavePool ```solidity -function setLiquidityProvider(address provider) external +event RemoveAavePool(address caller, address token) ``` -sets new liquidity provider address +Emitted when an Aave V3 Pool is removed for a payment token #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| provider | address | new liquidity provider address | +| caller | address | address of the caller | +| token | address | payment token address | -### setSwapperVault +### SetAaveDepositsEnabled ```solidity -function setSwapperVault(address newVault) external +event SetAaveDepositsEnabled(bool enabled) ``` -sets new underlying vault for swapper +Emitted when `aaveDepositsEnabled` flag is updated #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| newVault | address | | +| enabled | bool | Whether Aave deposits are enabled | -### _swapMToken1ToMToken2 +### SetAutoInvestFallbackEnabled ```solidity -function _swapMToken1ToMToken2(uint256 mToken1Amount) internal returns (uint256 mTokenAmount) +event SetAutoInvestFallbackEnabled(bool enabled) ``` -Transfers mToken1 to liquidity provider -Transfers mToken2 from liquidity provider to contract -Returns amount on mToken2 using exchange rates +Emitted when `autoInvestFallbackEnabled` flag is updated #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| mToken1Amount | uint256 | mToken1 token amount (decimals 18) | - -## RedemptionVaultWithUSTB - -Smart contract that handles redemptions using USTB +| enabled | bool | Whether fallback to raw transfer is enabled | -### ustbRedemption +### setAavePool ```solidity -contract IUSTBRedemption ustbRedemption +function setAavePool(address _token, address _aavePool) external ``` -USTB redemption contract address +Sets the Aave V3 Pool for a specific payment token -_Used to handle USTB redemptions when vault has insufficient USDC_ +#### Parameters -### initialize +| Name | Type | Description | +| ---- | ---- | ----------- | +| _token | address | payment token address | +| _aavePool | address | Aave V3 Pool address for this token | + +### removeAavePool ```solidity -function initialize(address _ac, struct MTokenInitParams _mTokenInitParams, struct ReceiversInitParams _receiversInitParams, struct InstantInitParams _instantInitParams, address _sanctionsList, uint256 _variationTolerance, uint256 _minAmount, struct FiatRedeptionInitParams _fiatRedemptionInitParams, address _requestRedeemer, address _ustbRedemption) external +function removeAavePool(address _token) external ``` -upgradeable pattern contract`s initializer +Removes the Aave V3 Pool for a specific payment token #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| _ac | address | address of MidasAccessControll contract | -| _mTokenInitParams | struct MTokenInitParams | init params for mToken | -| _receiversInitParams | struct ReceiversInitParams | init params for receivers | -| _instantInitParams | struct InstantInitParams | init params for instant operations | -| _sanctionsList | address | address of sanctionsList contract | -| _variationTolerance | uint256 | percent of prices diviation 1% = 100 | -| _minAmount | uint256 | basic min amount for operations | -| _fiatRedemptionInitParams | struct FiatRedeptionInitParams | params fiatAdditionalFee, fiatFlatFee, minFiatRedeemAmount | -| _requestRedeemer | address | address is designated for standard redemptions, allowing tokens to be pulled from this address | -| _ustbRedemption | address | USTB redemption contract address | +| _token | address | payment token address | -### _redeemInstant +### setAaveDepositsEnabled ```solidity -function _redeemInstant(address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount, address recipient) internal returns (struct RedemptionVault.CalcAndValidateRedeemResult calcResult, uint256 amountTokenOutWithoutFee) +function setAaveDepositsEnabled(bool enabled) external ``` -_Redeem mToken to the selected payment token if daily limit and allowance are not exceeded. -If USDC is the payment token and the contract doesn't have enough USDC, the USTB redemption flow will be triggered for the missing amount. -Burns mToken from the user. -Transfers fee in mToken to feeReceiver. -Transfers tokenOut to user._ +Updates `aaveDepositsEnabled` value #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tokenOut | address | token out address | -| amountMTokenIn | uint256 | amount of mToken to redeem | -| minReceiveAmount | uint256 | minimum expected amount of tokenOut to receive (decimals 18) | -| recipient | address | | +| enabled | bool | whether Aave auto-invest deposits are enabled | -### _checkAndRedeemUSTB +### setAutoInvestFallbackEnabled ```solidity -function _checkAndRedeemUSTB(address tokenOut, uint256 amountTokenOut) internal +function setAutoInvestFallbackEnabled(bool enabled) external ``` -Check if contract has enough USDC balance for redeem -if not, trigger USTB redemption flow to redeem exactly the missing amount +Updates `autoInvestFallbackEnabled` value #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tokenOut | address | tokenOut address | -| amountTokenOut | uint256 | amount of tokenOut needed | - -## ManageableVault +| enabled | bool | whether fallback to raw transfer is enabled on auto-invest failure | -Contract with base Vault methods - -### MANUAL_FULLFILMENT_TOKEN +### _instantTransferTokensToTokensReceiver ```solidity -address MANUAL_FULLFILMENT_TOKEN +function _instantTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal virtual ``` -address that represents off-chain USD bank transfer +_overrides instant deposit transfer hook to auto-invest into Aave_ -### STABLECOIN_RATE +### _requestTransferTokensToTokensReceiver ```solidity -uint256 STABLECOIN_RATE +function _requestTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal virtual ``` -stable coin static rate 1:1 USD in 18 decimals +_overrides request deposit transfer hook to auto-invest into Aave_ -### currentRequestId +## DepositVaultWithMToken -```solidity -struct Counters.Counter currentRequestId -``` +Smart contract that handles mToken minting and invests +proceeds into another mToken's DepositVault -last request id +_If `mTokenDepositsEnabled` is false, regular deposit flow is used_ -### ONE_HUNDRED_PERCENT +### mTokenDepositVault ```solidity -uint256 ONE_HUNDRED_PERCENT +contract IDepositVault mTokenDepositVault ``` -100 percent with base 100 - -_for example, 10% will be (10 * 100)%_ +Target mToken DepositVault for auto-invest -### MAX_UINT +### mTokenDepositsEnabled ```solidity -uint256 MAX_UINT +bool mTokenDepositsEnabled ``` -### mToken +Whether mToken auto-invest deposits are enabled + +_if false, regular deposit flow will be used_ + +### autoInvestFallbackEnabled ```solidity -contract IMTbill mToken +bool autoInvestFallbackEnabled ``` -mToken token +Whether to fall back to raw token transfer on auto-invest failure -### mTokenDataFeed +_if false, the transaction will revert when auto-invest fails_ + +### SetMTokenDepositVault ```solidity -contract IDataFeed mTokenDataFeed +event SetMTokenDepositVault(address caller, address newVault) ``` -mToken data feed contract +Emitted when the mToken DepositVault address is updated -### tokensReceiver +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | address of the caller | +| newVault | address | new mToken DepositVault address | + +### SetMTokenDepositsEnabled ```solidity -address tokensReceiver +event SetMTokenDepositsEnabled(bool enabled) ``` -address to which tokens and mTokens will be sent +Emitted when `mTokenDepositsEnabled` flag is updated -### instantFee +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| enabled | bool | Whether mToken deposits are enabled | + +### SetAutoInvestFallbackEnabled ```solidity -uint256 instantFee +event SetAutoInvestFallbackEnabled(bool enabled) ``` -_fee for initial operations 1% = 100_ +Emitted when `autoInvestFallbackEnabled` flag is updated + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| enabled | bool | Whether fallback to raw transfer is enabled | -### instantDailyLimit +### initialize ```solidity -uint256 instantDailyLimit +function initialize(struct CommonVaultInitParams _commonVaultInitParams, struct CommonVaultV2InitParams _commonVaultV2InitParams, uint256 _minMTokenAmountForFirstDeposit, uint256 _maxSupplyCap, address _mTokenDepositVault) external ``` -_daily limit for initial operations -if user exceed this limit he will need -to create requests_ +upgradeable pattern contract`s initializer + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _commonVaultInitParams | struct CommonVaultInitParams | init params for common vault | +| _commonVaultV2InitParams | struct CommonVaultV2InitParams | init params for common vault v2 | +| _minMTokenAmountForFirstDeposit | uint256 | min amount for first deposit in mToken | +| _maxSupplyCap | uint256 | max supply cap for mToken | +| _mTokenDepositVault | address | target mToken DepositVault address | -### dailyLimits +### setMTokenDepositVault ```solidity -mapping(uint256 => uint256) dailyLimits +function setMTokenDepositVault(address _mTokenDepositVault) external ``` -_mapping days (number from 1970) to limit amount_ +Sets the target mToken DepositVault address -### feeReceiver +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _mTokenDepositVault | address | new mToken DepositVault address | + +### setMTokenDepositsEnabled ```solidity -address feeReceiver +function setMTokenDepositsEnabled(bool enabled) external ``` -address to which fees will be sent +Updates `mTokenDepositsEnabled` value -### variationTolerance +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| enabled | bool | whether mToken auto-invest deposits are enabled | + +### setAutoInvestFallbackEnabled ```solidity -uint256 variationTolerance +function setAutoInvestFallbackEnabled(bool enabled) external ``` -variation tolerance of tokenOut rates for "safe" requests approve +Updates `autoInvestFallbackEnabled` value -### waivedFeeRestriction +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| enabled | bool | whether fallback to raw transfer is enabled on auto-invest failure | + +### _instantTransferTokensToTokensReceiver ```solidity -mapping(address => bool) waivedFeeRestriction +function _instantTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal virtual ``` -address restriction with zero fees +_overrides instant deposit transfer hook to auto-invest into target mToken DV_ -### _paymentTokens +### _requestTransferTokensToTokensReceiver ```solidity -struct EnumerableSetUpgradeable.AddressSet _paymentTokens +function _requestTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal virtual ``` -_tokens that can be used as USD representation_ +_overrides request deposit transfer hook to auto-invest into target mToken DV_ -### tokensConfig +## DepositVaultWithMorpho -```solidity -mapping(address => struct TokenConfig) tokensConfig -``` +Smart contract that handles mToken minting and invests +proceeds into Morpho Vaults -mapping, token address to token config +_If `morphoDepositsEnabled` is false, regular deposit flow is used_ -### minAmount +### morphoVaults ```solidity -uint256 minAmount +mapping(address => contract IMorphoVault) morphoVaults ``` -basic min operations amount +mapping payment token to Morpho Vault -### isFreeFromMinAmount +### morphoDepositsEnabled ```solidity -mapping(address => bool) isFreeFromMinAmount +bool morphoDepositsEnabled ``` -mapping, user address => is free frmo min amounts +Whether Morpho auto-invest deposits are enabled -### onlyVaultAdmin +_if false, regular deposit flow will be used_ + +### autoInvestFallbackEnabled ```solidity -modifier onlyVaultAdmin() +bool autoInvestFallbackEnabled ``` -_checks that msg.sender do have a vaultRole() role_ +Whether to fall back to raw token transfer on auto-invest failure -### __ManageableVault_init +_if false, the transaction will revert when auto-invest fails_ + +### SetMorphoVault ```solidity -function __ManageableVault_init(address _ac, struct MTokenInitParams _mTokenInitParams, struct ReceiversInitParams _receiversInitParams, struct InstantInitParams _instantInitParams, address _sanctionsList, uint256 _variationTolerance, uint256 _minAmount) internal +event SetMorphoVault(address caller, address token, address vault) ``` -_upgradeable pattern contract`s initializer_ +Emitted when a Morpho Vault is configured for a payment token #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| _ac | address | address of MidasAccessControll contract | -| _mTokenInitParams | struct MTokenInitParams | init params for mToken | -| _receiversInitParams | struct ReceiversInitParams | init params for receivers | -| _instantInitParams | struct InstantInitParams | init params for instant operations | -| _sanctionsList | address | address of sanctionsList contract | -| _variationTolerance | uint256 | percent of prices diviation 1% = 100 | -| _minAmount | uint256 | basic min amount for operations | +| caller | address | address of the caller | +| token | address | payment token address | +| vault | address | Morpho Vault address | -### withdrawToken +### RemoveMorphoVault ```solidity -function withdrawToken(address token, uint256 amount, address withdrawTo) external +event RemoveMorphoVault(address caller, address token) ``` -withdraws `amount` of a given `token` from the contract. -can be called only from permissioned actor. +Emitted when a Morpho Vault is removed for a payment token #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| token | address | token address | -| amount | uint256 | token amount | -| withdrawTo | address | withdraw destination address | +| caller | address | address of the caller | +| token | address | payment token address | -### addPaymentToken +### SetMorphoDepositsEnabled ```solidity -function addPaymentToken(address token, address dataFeed, uint256 tokenFee, bool stable) external +event SetMorphoDepositsEnabled(bool enabled) ``` -adds a token to the stablecoins list. -can be called only from permissioned actor. - -_reverts if token is already added_ +Emitted when `morphoDepositsEnabled` flag is updated #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| token | address | token address | -| dataFeed | address | dataFeed address | -| tokenFee | uint256 | | -| stable | bool | is stablecoin flag | +| enabled | bool | Whether Morpho deposits are enabled | -### removePaymentToken +### SetAutoInvestFallbackEnabled ```solidity -function removePaymentToken(address token) external +event SetAutoInvestFallbackEnabled(bool enabled) ``` -removes a token from stablecoins list. -can be called only from permissioned actor. - -_reverts if token is not presented_ +Emitted when `autoInvestFallbackEnabled` flag is updated #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| token | address | token address | +| enabled | bool | Whether fallback to raw transfer is enabled | -### changeTokenAllowance +### setMorphoVault ```solidity -function changeTokenAllowance(address token, uint256 allowance) external +function setMorphoVault(address _token, address _morphoVault) external ``` -set new token allowance. -if MAX_UINT = infinite allowance -prev allowance rewrites by new -can be called only from permissioned actor. - -_reverts if new allowance zero_ +Sets the Morpho Vault for a specific payment token #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| token | address | token address | -| allowance | uint256 | new allowance (decimals 18) | +| _token | address | payment token address | +| _morphoVault | address | Morpho Vault (ERC-4626) address for this token | -### changeTokenFee +### removeMorphoVault ```solidity -function changeTokenFee(address token, uint256 fee) external +function removeMorphoVault(address _token) external ``` -set new token fee. -can be called only from permissioned actor. - -_reverts if new fee > 100%_ +Removes the Morpho Vault for a specific payment token #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| token | address | token address | -| fee | uint256 | new fee percent 1% = 100 | +| _token | address | payment token address | -### setVariationTolerance +### setMorphoDepositsEnabled ```solidity -function setVariationTolerance(uint256 tolerance) external +function setMorphoDepositsEnabled(bool enabled) external ``` -set new prices diviation percent. -can be called only from permissioned actor. - -_reverts if new tolerance zero_ +Updates `morphoDepositsEnabled` value #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tolerance | uint256 | new prices diviation percent 1% = 100 | +| enabled | bool | whether Morpho auto-invest deposits are enabled | -### setMinAmount +### setAutoInvestFallbackEnabled ```solidity -function setMinAmount(uint256 newAmount) external +function setAutoInvestFallbackEnabled(bool enabled) external ``` -set new min amount. -can be called only from permissioned actor. +Updates `autoInvestFallbackEnabled` value #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| newAmount | uint256 | min amount for operations in mToken | +| enabled | bool | whether fallback to raw transfer is enabled on auto-invest failure | -### addWaivedFeeAccount +### _instantTransferTokensToTokensReceiver ```solidity -function addWaivedFeeAccount(address account) external +function _instantTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal virtual ``` -adds a account to waived fee restriction. -can be called only from permissioned actor. +_overrides instant deposit transfer hook to auto-invest into Morpho_ -_reverts if account is already added_ +### _requestTransferTokensToTokensReceiver -#### Parameters +```solidity +function _requestTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal virtual +``` -| Name | Type | Description | -| ---- | ---- | ----------- | -| account | address | user address | +_overrides request deposit transfer hook to auto-invest into Morpho_ -### removeWaivedFeeAccount +## DepositVaultWithUSTB + +Smart contract that handles mToken minting and invests +proceeds into USTB + +### ustb ```solidity -function removeWaivedFeeAccount(address account) external +address ustb ``` -removes a account from waived fee restriction. -can be called only from permissioned actor. +USTB token address -_reverts if account is already removed_ +### ustbDepositsEnabled -#### Parameters +```solidity +bool ustbDepositsEnabled +``` -| Name | Type | Description | -| ---- | ---- | ----------- | -| account | address | user address | +Whether USTB deposits are enabled -### setFeeReceiver +_if false, regular deposit flow will be used_ + +### SetUstbDepositsEnabled ```solidity -function setFeeReceiver(address receiver) external +event SetUstbDepositsEnabled(bool enabled) ``` -set new reciever for fees. -can be called only from permissioned actor. - -_reverts address zero or equal address(this)_ +Emitted when `ustbDepositsEnabled` flag is updated #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| receiver | address | | +| enabled | bool | Whether USTB deposits are enabled | -### setTokensReceiver +### initialize ```solidity -function setTokensReceiver(address receiver) external +function initialize(struct CommonVaultInitParams _commonVaultInitParams, struct CommonVaultV2InitParams _commonVaultV2InitParams, uint256 _minMTokenAmountForFirstDeposit, uint256 _maxSupplyCap, address _ustb) external ``` -set new reciever for tokens. -can be called only from permissioned actor. - -_reverts address zero or equal address(this)_ +upgradeable pattern contract`s initializer #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| receiver | address | | +| _commonVaultInitParams | struct CommonVaultInitParams | init params for common vault | +| _commonVaultV2InitParams | struct CommonVaultV2InitParams | init params for common vault v2 | +| _minMTokenAmountForFirstDeposit | uint256 | min amount for first deposit in mToken | +| _maxSupplyCap | uint256 | | +| _ustb | address | USTB token address | -### setInstantFee +### setUstbDepositsEnabled ```solidity -function setInstantFee(uint256 newInstantFee) external +function setUstbDepositsEnabled(bool enabled) external ``` -set operation fee percent. -can be called only from permissioned actor. +Updates `ustbDepositsEnabled` value #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| newInstantFee | uint256 | new instant operations fee percent 1& = 100 | +| enabled | bool | whether USTB deposits are enabled | -### setInstantDailyLimit +### _instantTransferTokensToTokensReceiver ```solidity -function setInstantDailyLimit(uint256 newInstantDailyLimit) external +function _instantTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal virtual ``` -set operation daily limit. -can be called only from permissioned actor. +_overrides original transfer to tokens receiver function +in case of USTB deposits are disabled or invest token is not supported +by USTB, it will act as the original transfer +otherwise it will take payment tokens from user, invest them into USTB +and will transfer USTB to tokens receiver_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| newInstantDailyLimit | uint256 | new operation daily limit (decimals 18) | +| tokenIn | address | token address | +| amountToken | uint256 | amount of tokens to transfer in base18 | +| tokensDecimals | uint256 | decimals of tokens | -### freeFromMinAmount +## ManageableVault + +Contract with base Vault methods + +### STABLECOIN_RATE ```solidity -function freeFromMinAmount(address user, bool enable) external +uint256 STABLECOIN_RATE ``` -frees given `user` from the minimal deposit -amount validation in `initiateDepositRequest` +stable coin static rate 1:1 USD in 18 decimals -#### Parameters +### currentRequestId -| Name | Type | Description | -| ---- | ---- | ----------- | -| user | address | address of user | -| enable | bool | | +```solidity +struct Counters.Counter currentRequestId +``` -### getPaymentTokens +last request id + +### ONE_HUNDRED_PERCENT ```solidity -function getPaymentTokens() external view returns (address[]) +uint256 ONE_HUNDRED_PERCENT ``` -returns array of stablecoins supported by the vault -can be called only from permissioned actor. - -#### Return Values +100 percent with base 100 -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | address[] | paymentTokens array of payment tokens | +_for example, 10% will be (10 * 100)%_ -### vaultRole +### mToken ```solidity -function vaultRole() public view virtual returns (bytes32) +contract IMToken mToken ``` -AC role of vault administrator +mToken token -#### Return Values +### mTokenDataFeed -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | bytes32 | role bytes32 role | +```solidity +contract IDataFeed mTokenDataFeed +``` -### sanctionsListAdminRole +mToken data feed contract + +### tokensReceiver ```solidity -function sanctionsListAdminRole() public view virtual returns (bytes32) +address tokensReceiver ``` -AC role of sanctions list admin +address to which tokens and mTokens will be sent -_address that have this role can use `setSanctionsList`_ +### instantFee -#### Return Values +```solidity +uint256 instantFee +``` -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | bytes32 | role bytes32 role | +_fee for initial operations 1% = 100_ -### pauseAdminRole +### feeReceiver ```solidity -function pauseAdminRole() public view returns (bytes32) +address feeReceiver ``` -_virtual function to determine pauseAdmin role_ +address to which fees will be sent -### _tokenTransferFromUser +### variationTolerance ```solidity -function _tokenTransferFromUser(address token, address to, uint256 amount, uint256 tokenDecimals) internal +uint256 variationTolerance ``` -_do safeTransferFrom on a given token -and converts `amount` from base18 -to amount with a correct precision. Sends tokens -from `msg.sender` to `tokensReceiver`_ +variation tolerance of tokenOut rates for "safe" requests approve -#### Parameters +### waivedFeeRestriction -| Name | Type | Description | -| ---- | ---- | ----------- | -| token | address | address of token | -| to | address | address of user | -| amount | uint256 | amount of `token` to transfer from `user` (decimals 18) | -| tokenDecimals | uint256 | token decimals | +```solidity +mapping(address => bool) waivedFeeRestriction +``` -### _tokenTransferFromTo +address restriction with zero fees + +### _paymentTokens ```solidity -function _tokenTransferFromTo(address token, address from, address to, uint256 amount, uint256 tokenDecimals) internal +struct EnumerableSetUpgradeable.AddressSet _paymentTokens ``` -_do safeTransferFrom on a given token -and converts `amount` from base18 -to amount with a correct precision._ +_tokens that can be used as USD representation_ -#### Parameters +### tokensConfig -| Name | Type | Description | -| ---- | ---- | ----------- | -| token | address | address of token | -| from | address | address | -| to | address | address | -| amount | uint256 | amount of `token` to transfer from `user` | -| tokenDecimals | uint256 | token decimals | +```solidity +mapping(address => struct TokenConfig) tokensConfig +``` -### _tokenTransferToUser +mapping, token address to token config + +### minAmount ```solidity -function _tokenTransferToUser(address token, address to, uint256 amount, uint256 tokenDecimals) internal +uint256 minAmount ``` -_do safeTransfer on a given token -and converts `amount` from base18 -to amount with a correct precision. Sends tokens -from `contract` to `user`_ +basic min operations amount -#### Parameters +### isFreeFromMinAmount -| Name | Type | Description | -| ---- | ---- | ----------- | -| token | address | address of token | -| to | address | address of user | -| amount | uint256 | amount of `token` to transfer from `user` (decimals 18) | -| tokenDecimals | uint256 | token decimals | +```solidity +mapping(address => bool) isFreeFromMinAmount +``` -### _tokenDecimals +mapping, user address => is free frmo min amounts + +### minInstantFee ```solidity -function _tokenDecimals(address token) internal view returns (uint8) +uint64 minInstantFee ``` -_retreives decimals of a given `token`_ - -#### Parameters +minimum instant fee -| Name | Type | Description | -| ---- | ---- | ----------- | -| token | address | address of token | +### maxInstantFee -#### Return Values +```solidity +uint64 maxInstantFee +``` -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | uint8 | decimals decinmals value of a given `token` | +maximum instant fee -### _requireTokenExists +### maxInstantShare ```solidity -function _requireTokenExists(address token) internal view virtual +uint64 maxInstantShare ``` -_checks that `token` is presented in `_paymentTokens`_ +maximum instant share value in basis points (100 = 1%) -#### Parameters +### maxApproveRequestId -| Name | Type | Description | -| ---- | ---- | ----------- | -| token | address | address of token | +```solidity +uint256 maxApproveRequestId +``` -### _requireAndUpdateLimit +max requestId that can be approved + +### limitConfigs ```solidity -function _requireAndUpdateLimit(uint256 amount) internal +mapping(uint256 => struct LimitConfig) limitConfigs ``` -_check if operation exceed daily limit and update limit data_ +mapping, window duration in seconds => limit config -#### Parameters +### validateVaultAdminAccess -| Name | Type | Description | -| ---- | ---- | ----------- | -| amount | uint256 | operation amount (decimals 18) | +```solidity +modifier validateVaultAdminAccess() +``` -### _requireAndUpdateAllowance +_checks that msg.sender do have a vaultRole() role +and validates if function is not paused_ + +### validateUserAccess ```solidity -function _requireAndUpdateAllowance(address token, uint256 amount) internal +modifier validateUserAccess(address recipient) ``` -_check if operation exceed token allowance and update allowance_ +_validate msg.sender and recipient access, validates if function is not paused_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| token | address | address of token | -| amount | uint256 | operation amount (decimals 18) | +| recipient | address | recipient address | -### _getFeeAmount +### __ManageableVault_init ```solidity -function _getFeeAmount(address sender, address token, uint256 amount, bool isInstant, uint256 additionalFee) internal view returns (uint256) +function __ManageableVault_init(struct CommonVaultInitParams _commonVaultInitParams) internal ``` -_returns calculated fee amount depends on parameters -if additionalFee not zero, token fee replaced with additionalFee_ +_upgradeable pattern contract`s initializer_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| sender | address | sender address | -| token | address | token address | -| amount | uint256 | amount of token (decimals 18) | -| isInstant | bool | is instant operation | -| additionalFee | uint256 | fee for fiat operations | - -#### Return Values - -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | uint256 | fee amount of input token | +| _commonVaultInitParams | struct CommonVaultInitParams | init params for common vault | -### _requireVariationTolerance +### __ManageableVault_initV2 ```solidity -function _requireVariationTolerance(uint256 prevPrice, uint256 newPrice) internal view +function __ManageableVault_initV2(struct CommonVaultV2InitParams _commonVaultV2InitParams) internal ``` -_check if prev and new prices diviation fit variationTolerance_ +_upgradeable pattern contract`s initializer_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| prevPrice | uint256 | previous rate | -| newPrice | uint256 | new rate | +| _commonVaultV2InitParams | struct CommonVaultV2InitParams | init params for common vault v2 | -### _validateUserAccess +### addPaymentToken ```solidity -function _validateUserAccess(address user) internal view +function addPaymentToken(address token, address dataFeed, uint256 tokenFee, uint256 allowance, bool stable) external ``` -### _truncate - -```solidity -function _truncate(uint256 value, uint256 decimals) internal pure returns (uint256) -``` - -_convert value to inputted decimals precision_ +adds a token to the stablecoins list. +can be called only from permissioned actor. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| value | uint256 | value for format | -| decimals | uint256 | decimals | - -#### Return Values - -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | uint256 | converted amount | +| token | address | token address | +| dataFeed | address | dataFeed address | +| tokenFee | uint256 | | +| allowance | uint256 | token allowance (decimals 18) | +| stable | bool | is stablecoin flag | -### _validateFee +### removePaymentToken ```solidity -function _validateFee(uint256 fee, bool checkMin) internal pure +function removePaymentToken(address token) external ``` -_check if fee <= 100% and check > 0 if needs_ +removes a token from stablecoins list. +can be called only from permissioned actor. + +_reverts if token is not presented_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| fee | uint256 | fee value | -| checkMin | bool | if need to check minimum | +| token | address | token address | -### _validateAddress +### changeTokenAllowance ```solidity -function _validateAddress(address addr, bool selfCheck) internal view +function changeTokenAllowance(address token, uint256 allowance) external ``` -_check if address not zero and not address(this)_ +set new token allowance. +if type(uint256).max = infinite allowance +prev allowance rewrites by new +can be called only from permissioned actor. + +_reverts if new allowance zero_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| addr | address | address to check | -| selfCheck | bool | check if address not address(this) | +| token | address | token address | +| allowance | uint256 | new allowance (decimals 18) | -### _getTokenRate +### changeTokenFee ```solidity -function _getTokenRate(address dataFeed, bool stable) internal view virtual returns (uint256) +function changeTokenFee(address token, uint256 fee) external ``` -_get token rate depends on data feed and stablecoin flag_ +set new token fee. +can be called only from permissioned actor. + +_reverts if new fee > 100%_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| dataFeed | address | address of dataFeed from token config | -| stable | bool | is stablecoin | - -## MidasInitializable - -Base Initializable contract that implements constructor -that calls _disableInitializers() to prevent -initialization of implementation contract +| token | address | token address | +| fee | uint256 | new fee percent 1% = 100 | -### constructor +### setVariationTolerance ```solidity -constructor() internal +function setVariationTolerance(uint256 tolerance) external ``` -## WithSanctionsList - -Base contract that uses sanctions oracle from -Chainalysis to check that user is not sanctioned +set new prices diviation percent. +can be called only from permissioned actor. -### sanctionsList +_reverts if new tolerance zero_ -```solidity -address sanctionsList -``` +#### Parameters -address of Chainalysis sanctions oracle +| Name | Type | Description | +| ---- | ---- | ----------- | +| tolerance | uint256 | new prices diviation percent 1% = 100 | -### SetSanctionsList +### setMinAmount ```solidity -event SetSanctionsList(address caller, address newSanctionsList) +function setMinAmount(uint256 newAmount) external ``` +set new min amount. +can be called only from permissioned actor. + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| caller | address | function caller (msg.sender) | -| newSanctionsList | address | new address of `sanctionsList` | +| newAmount | uint256 | min amount for operations in mToken | -### onlyNotSanctioned +### addWaivedFeeAccount ```solidity -modifier onlyNotSanctioned(address user) +function addWaivedFeeAccount(address account) external ``` -_checks that a given `user` is not sanctioned_ +adds a account to waived fee restriction. +can be called only from permissioned actor. -### __WithSanctionsList_init +_reverts if account is already added_ -```solidity -function __WithSanctionsList_init(address _accesControl, address _sanctionsList) internal -``` +#### Parameters -_upgradeable pattern contract`s initializer_ +| Name | Type | Description | +| ---- | ---- | ----------- | +| account | address | user address | -### __WithSanctionsList_init_unchained +### removeWaivedFeeAccount ```solidity -function __WithSanctionsList_init_unchained(address _sanctionsList) internal +function removeWaivedFeeAccount(address account) external ``` -_upgradeable pattern contract`s initializer unchained_ - -### setSanctionsList - -```solidity -function setSanctionsList(address newSanctionsList) external -``` +removes a account from waived fee restriction. +can be called only from permissioned actor. -updates `sanctionsList` address. -can be called only from permissioned actor that have -`sanctionsListAdminRole()` role +_reverts if account is already removed_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| newSanctionsList | address | new sanctions list address | +| account | address | user address | -### sanctionsListAdminRole +### setFeeReceiver ```solidity -function sanctionsListAdminRole() public view virtual returns (bytes32) +function setFeeReceiver(address receiver) external ``` -AC role of sanctions list admin +set new receiver for fees. +can be called only from permissioned actor. -_address that have this role can use `setSanctionsList`_ +_reverts address zero or equal address(this)_ -#### Return Values +#### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | bytes32 | role bytes32 role | - -## Blacklistable - -Base contract that implements basic functions and modifiers -to work with blacklistable +| receiver | address | | -### onlyNotBlacklisted +### setTokensReceiver ```solidity -modifier onlyNotBlacklisted(address account) +function setTokensReceiver(address receiver) external ``` -_checks that a given `account` doesnt -have BLACKLISTED_ROLE_ +set new receiver for tokens. +can be called only from permissioned actor. -### __Blacklistable_init +_reverts address zero or equal address(this)_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| receiver | address | | + +### setInstantFee ```solidity -function __Blacklistable_init(address _accessControl) internal +function setInstantFee(uint256 newInstantFee) external ``` -_upgradeable pattern contract`s initializer_ +set operation fee percent. +can be called only from permissioned actor. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | MidasAccessControl contract address | +| newInstantFee | uint256 | new instant operations fee percent 1& = 100 | -### __Blacklistable_init_unchained +### setMinMaxInstantFee ```solidity -function __Blacklistable_init_unchained() internal +function setMinMaxInstantFee(uint64 newMinInstantFee, uint64 newMaxInstantFee) external ``` -_upgradeable pattern contract`s initializer unchained_ +set new minimum/maximum instant fee -### _onlyNotBlacklisted +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newMinInstantFee | uint64 | new minimum instant fee | +| newMaxInstantFee | uint64 | new maximum instant fee | + +### setMaxInstantShare ```solidity -function _onlyNotBlacklisted(address account) internal view +function setMaxInstantShare(uint64 newMaxInstantShare) external ``` -_checks that a given `account` doesnt -have BLACKLISTED_ROLE_ +set maximum instant share value in basis points (100 = 1%) -## Greenlistable +#### Parameters -Base contract that implements basic functions and modifiers -to work with greenlistable +| Name | Type | Description | +| ---- | ---- | ----------- | +| newMaxInstantShare | uint64 | new maximum instant share value in basis points (100 = 1%) | -### GREENLIST_TOGGLER_ROLE +### setMaxApproveRequestId ```solidity -bytes32 GREENLIST_TOGGLER_ROLE +function setMaxApproveRequestId(uint256 newMaxApproveRequestId) external ``` -actor that can change green list enable - -### greenlistEnabled +sets max requestId that can be approved -```solidity -bool greenlistEnabled -``` +#### Parameters -is greenlist enabled +| Name | Type | Description | +| ---- | ---- | ----------- | +| newMaxApproveRequestId | uint256 | new max requestId that can be approved | -### SetGreenlistEnable +### setInstantLimitConfig ```solidity -event SetGreenlistEnable(address sender, bool enable) +function setInstantLimitConfig(uint256 window, uint256 limit) external ``` -### onlyGreenlisted +set operation limit configs. +can be called only from permissioned actor. -```solidity -modifier onlyGreenlisted(address account) -``` +#### Parameters -_checks that a given `account` -have `greenlistedRole()`_ +| Name | Type | Description | +| ---- | ---- | ----------- | +| window | uint256 | window duration in seconds | +| limit | uint256 | limit amount per window | -### onlyAlwaysGreenlisted +### removeInstantLimitConfig ```solidity -modifier onlyAlwaysGreenlisted(address account) +function removeInstantLimitConfig(uint256 window) external ``` -_checks that a given `account` -have `greenlistedRole()` -do the check even if greenlist check is off_ +remove operation limit config. +can be called only from permissioned actor. -### __Greenlistable_init +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| window | uint256 | window duration in seconds | + +### freeFromMinAmount ```solidity -function __Greenlistable_init(address _accessControl) internal +function freeFromMinAmount(address user, bool enable) external ``` -_upgradeable pattern contract`s initializer_ +frees given `user` from the minimal deposit +amount validation in `initiateDepositRequest` #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | MidasAccessControl contract address | +| user | address | address of user | +| enable | bool | | -### __Greenlistable_init_unchained +### withdrawToken ```solidity -function __Greenlistable_init_unchained() internal +function withdrawToken(address token, uint256 amount) external ``` -_upgradeable pattern contract`s initializer unchained_ +withdraws `amount` of a given `token` from the contract +to the `tokensReceiver` address -### setGreenlistEnable +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| token | address | token address | +| amount | uint256 | token amount | + +### getPaymentTokens ```solidity -function setGreenlistEnable(bool enable) external +function getPaymentTokens() external view returns (address[]) ``` -enable or disable greenlist. +returns array of stablecoins supported by the vault can be called only from permissioned actor. -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| enable | bool | enable | +| [0] | address[] | paymentTokens array of payment tokens | -### greenlistedRole +### getInstantLimitStatuses ```solidity -function greenlistedRole() public view virtual returns (bytes32) +function getInstantLimitStatuses() external view returns (uint256[] windows, struct LimitConfig[] configs) ``` -AC role of a greenlist +returns array of limit configs #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | bytes32 | role bytes32 role | +| windows | uint256[] | array of limit config windows | +| configs | struct LimitConfig[] | array of limit configs | -### greenlistTogglerRole +### vaultRole ```solidity -function greenlistTogglerRole() public view virtual returns (bytes32) +function vaultRole() public view virtual returns (bytes32) ``` -AC role of a greenlist +AC role of vault administrator #### Return Values @@ -1740,3608 +1812,16159 @@ AC role of a greenlist | ---- | ---- | ----------- | | [0] | bytes32 | role bytes32 role | -### _onlyGreenlistToggler +### _tokenTransferFromUser ```solidity -function _onlyGreenlistToggler(address account) internal view +function _tokenTransferFromUser(address token, address to, uint256 amount, uint256 tokenDecimals) internal returns (uint256 transferAmount) ``` -_checks that a given `account` -have a `greenlistTogglerRole()`_ - -## MidasAccessControl - -Smart contract that stores all roles for Midas project - -### initialize +_do safeTransferFrom on a given token +and converts `amount` from base18 +to amount with a correct precision. Sends tokens +from `msg.sender` to `tokensReceiver`_ -```solidity -function initialize() external -``` +#### Parameters -upgradeable pattern contract`s initializer +| Name | Type | Description | +| ---- | ---- | ----------- | +| token | address | address of token | +| to | address | address of user | +| amount | uint256 | amount of `token` to transfer from `user` (decimals 18) | +| tokenDecimals | uint256 | token decimals | -### grantRoleMult +### _tokenTransferToUser ```solidity -function grantRoleMult(bytes32[] roles, address[] addresses) external +function _tokenTransferToUser(address token, address to, uint256 amount, uint256 tokenDecimals) internal ``` -grant multiple roles to multiple users -in one transaction - -_length`s of 2 arays should match_ +_do safeTransfer on a given token +and converts `amount` from base18 +to amount with a correct precision. Sends tokens +from `contract` to `user`_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| roles | bytes32[] | array of bytes32 roles | -| addresses | address[] | array of user addresses | +| token | address | address of token | +| to | address | address of user | +| amount | uint256 | amount of `token` to transfer from `user` (decimals 18) | +| tokenDecimals | uint256 | token decimals | -### revokeRoleMult +### _tokenTransferFromTo ```solidity -function revokeRoleMult(bytes32[] roles, address[] addresses) external +function _tokenTransferFromTo(address token, address from, address to, uint256 amount, uint256 tokenDecimals) internal returns (uint256 transferAmount) ``` -revoke multiple roles from multiple users -in one transaction - -_length`s of 2 arays should match_ +_do safeTransfer or safeTransferFrom on a given token +and converts `amount` from base18 +to amount with a correct precision._ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| roles | bytes32[] | array of bytes32 roles | -| addresses | address[] | array of user addresses | +| token | address | address of token | +| from | address | address. If its address(this) the safeTransfer will be used instead of safeTransferFrom | +| to | address | address | +| amount | uint256 | amount of `token` to transfer from `user` | +| tokenDecimals | uint256 | token decimals | -### renounceRole +### _tokenDecimals ```solidity -function renounceRole(bytes32, address) public pure +function _tokenDecimals(address token) internal view returns (uint8) ``` -## MidasAccessControlRoles +_retreives decimals of a given `token`_ -Base contract that stores all roles descriptors +#### Parameters -### GREENLIST_OPERATOR_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | +| token | address | address of token | -```solidity -bytes32 GREENLIST_OPERATOR_ROLE -``` +#### Return Values -actor that can change green list statuses of addresses +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint8 | decimals decinmals value of a given `token` | -### BLACKLIST_OPERATOR_ROLE +### _requireTokenExists ```solidity -bytes32 BLACKLIST_OPERATOR_ROLE +function _requireTokenExists(address token) internal view virtual ``` -actor that can change black list statuses of addresses - -### M_TBILL_MINT_OPERATOR_ROLE +_checks that `token` is presented in `_paymentTokens`_ -```solidity -bytes32 M_TBILL_MINT_OPERATOR_ROLE -``` +#### Parameters -actor that can mint mTBILL +| Name | Type | Description | +| ---- | ---- | ----------- | +| token | address | address of token | -### M_TBILL_BURN_OPERATOR_ROLE +### _requireAndUpdateLimit ```solidity -bytes32 M_TBILL_BURN_OPERATOR_ROLE +function _requireAndUpdateLimit(uint256 amount) internal ``` -actor that can burn mTBILL - -### M_TBILL_PAUSE_OPERATOR_ROLE +_check if operation exceed daily limit and update limit data_ -```solidity -bytes32 M_TBILL_PAUSE_OPERATOR_ROLE -``` +#### Parameters -actor that can pause mTBILL +| Name | Type | Description | +| ---- | ---- | ----------- | +| amount | uint256 | operation amount (decimals 18) | -### DEPOSIT_VAULT_ADMIN_ROLE +### _requireAndUpdateAllowance ```solidity -bytes32 DEPOSIT_VAULT_ADMIN_ROLE +function _requireAndUpdateAllowance(address token, uint256 amount) internal ``` -actor that have admin rights in deposit vault +_check if operation exceed token allowance and update allowance_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| token | address | address of token | +| amount | uint256 | operation amount (decimals 18) | -### REDEMPTION_VAULT_ADMIN_ROLE +### _getFeeAmount ```solidity -bytes32 REDEMPTION_VAULT_ADMIN_ROLE +function _getFeeAmount(uint256 feePercent, uint256 amount) internal view returns (uint256) ``` -actor that have admin rights in redemption vault +_returns calculated fee amount depends on the provided fee percent and amount_ -### GREENLISTED_ROLE +#### Parameters -```solidity -bytes32 GREENLISTED_ROLE -``` +| Name | Type | Description | +| ---- | ---- | ----------- | +| feePercent | uint256 | fee percent | +| amount | uint256 | amount of token (decimals 18) | -actor that is greenlisted +#### Return Values -### BLACKLISTED_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | feeAmount calculated fee amount | + +### _getFee ```solidity -bytes32 BLACKLISTED_ROLE +function _getFee(address sender, address token, bool isInstant) internal view returns (uint256 feePercent) ``` -actor that is blacklisted +_returns calculated fee percent depends on parameters_ -## Pausable +#### Parameters -Base contract that implements basic functions and modifiers -with pause functionality +| Name | Type | Description | +| ---- | ---- | ----------- | +| sender | address | sender address | +| token | address | token address | +| isInstant | bool | is instant operation | -### fnPaused +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| feePercent | uint256 | calculated fee percent | + +### _validateInstantFee ```solidity -mapping(bytes4 => bool) fnPaused +function _validateInstantFee() internal view ``` -### PauseFn +_validates instant fee is within the range of min/max instant fee_ + +### _requireVariationTolerance ```solidity -event PauseFn(address caller, bytes4 fn) +function _requireVariationTolerance(uint256 prevPrice, uint256 newPrice) internal view ``` +_check if prev and new prices diviation fit variationTolerance_ + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| caller | address | caller address (msg.sender) | -| fn | bytes4 | function id | +| prevPrice | uint256 | previous rate | +| newPrice | uint256 | new rate | -### UnpauseFn +### _validateUserAccess ```solidity -event UnpauseFn(address caller, bytes4 fn) +function _validateUserAccess(address user, bool validatePaused) internal view ``` +_validate user access_ + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| caller | address | caller address (msg.sender) | -| fn | bytes4 | function id | +| user | address | user address | +| validatePaused | bool | if true, validates if function is not paused | -### whenFnNotPaused +### _validateUserAccess ```solidity -modifier whenFnNotPaused(bytes4 fn) +function _validateUserAccess(address user, address recipient) internal view ``` -### onlyPauseAdmin +_validate user access and validates if function is not paused_ -```solidity -modifier onlyPauseAdmin() -``` +#### Parameters -_checks that a given `account` -has a determinedPauseAdminRole_ +| Name | Type | Description | +| ---- | ---- | ----------- | +| user | address | user address | +| recipient | address | recipient address | -### __Pausable_init +### _validatePauseAdminAccess ```solidity -function __Pausable_init(address _accessControl) internal +function _validatePauseAdminAccess(address account) internal view ``` -_upgradeable pattern contract`s initializer_ +_validates that the caller has access to pause functions_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | MidasAccessControl contract address | +| account | address | account address | -### pause +### _validateGreenlistableAdminAccess ```solidity -function pause() external +function _validateGreenlistableAdminAccess(address account) internal view ``` -### unpause +_checks that a given `account` has access to greenlistable functions_ + +### _validateSanctionListAdminAccess ```solidity -function unpause() external +function _validateSanctionListAdminAccess(address account) internal view ``` -### pauseFn +_validates that the caller has access to sanctions list functions_ + +### _truncate ```solidity -function pauseFn(bytes4 fn) external +function _truncate(uint256 value, uint256 decimals) internal pure returns (uint256) ``` -_pause specific function_ +_convert value to inputted decimals precision_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| fn | bytes4 | function id | +| value | uint256 | value for format | +| decimals | uint256 | decimals | -### unpauseFn +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | converted amount | + +### _validateFee ```solidity -function unpauseFn(bytes4 fn) external +function _validateFee(uint256 fee, bool checkMin) internal pure ``` -_unpause specific function_ +_check if fee <= 100% and check > 0 if needs_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| fn | bytes4 | function id | +| fee | uint256 | fee value | +| checkMin | bool | if need to check minimum | -### pauseAdminRole +### _validateAddress ```solidity -function pauseAdminRole() public view virtual returns (bytes32) +function _validateAddress(address addr, bool selfCheck) internal view ``` -_virtual function to determine pauseAdmin role_ +_check if address not zero and not address(this)_ -## WithMidasAccessControl +#### Parameters -Base contract that consumes MidasAccessControl +| Name | Type | Description | +| ---- | ---- | ----------- | +| addr | address | address to check | +| selfCheck | bool | check if address not address(this) | -### DEFAULT_ADMIN_ROLE +### _getTokenRate ```solidity -bytes32 DEFAULT_ADMIN_ROLE +function _getTokenRate(address dataFeed, bool stable) internal view virtual returns (uint256) ``` -admin role - -### accessControl +_get token rate depends on data feed and stablecoin flag_ -```solidity -contract MidasAccessControl accessControl -``` +#### Parameters -MidasAccessControl contract address +| Name | Type | Description | +| ---- | ---- | ----------- | +| dataFeed | address | address of dataFeed from token config | +| stable | bool | is stablecoin | -### onlyRole +### _getMTokenRate ```solidity -modifier onlyRole(bytes32 role, address account) +function _getMTokenRate() internal view returns (uint256 mTokenRate) ``` -_checks that given `address` have `role`_ - -### onlyNotRole +_gets and validates mToken rate_ -```solidity -modifier onlyNotRole(bytes32 role, address account) -``` +#### Return Values -_checks that given `address` do not have `role`_ +| Name | Type | Description | +| ---- | ---- | ----------- | +| mTokenRate | uint256 | mToken rate | -### __WithMidasAccessControl_init +### _getPTokenRate ```solidity -function __WithMidasAccessControl_init(address _accessControl) internal +function _getPTokenRate(address token) internal view returns (uint256 tokenRate) ``` -_upgradeable pattern contract`s initializer_ +_gets and validates pToken rate_ -### _onlyRole - -```solidity -function _onlyRole(bytes32 role, address account) internal view -``` - -_checks that given `address` have `role`_ +#### Parameters -### _onlyNotRole +| Name | Type | Description | +| ---- | ---- | ----------- | +| token | address | address of pToken | -```solidity -function _onlyNotRole(bytes32 role, address account) internal view -``` +#### Return Values -_checks that given `address` do not have `role`_ +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenRate | uint256 | token rate | -## EUsdMidasAccessControlRoles +## MidasInitializable -Base contract that stores all roles descriptors for eUSD contracts +Base Initializable contract that implements constructor +that calls _disableInitializers() to prevent +initialization of implementation contract -### E_USD_VAULT_ROLES_OPERATOR +### constructor ```solidity -bytes32 E_USD_VAULT_ROLES_OPERATOR +constructor() internal ``` -actor that can manage vault admin roles +## WithSanctionsList -### E_USD_DEPOSIT_VAULT_ADMIN_ROLE +Base contract that uses sanctions oracle from +Chainalysis to check that user is not sanctioned + +### sanctionsList ```solidity -bytes32 E_USD_DEPOSIT_VAULT_ADMIN_ROLE +address sanctionsList ``` -actor that can manage EUsdDepositVault +address of Chainalysis sanctions oracle -### E_USD_REDEMPTION_VAULT_ADMIN_ROLE +### SetSanctionsList ```solidity -bytes32 E_USD_REDEMPTION_VAULT_ADMIN_ROLE +event SetSanctionsList(address caller, address newSanctionsList) ``` -actor that can manage EUsdRedemptionVault +#### Parameters -### E_USD_GREENLIST_OPERATOR_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | function caller (msg.sender) | +| newSanctionsList | address | new address of `sanctionsList` | + +### onlyNotSanctioned ```solidity -bytes32 E_USD_GREENLIST_OPERATOR_ROLE +modifier onlyNotSanctioned(address user) ``` -actor that can change eUSD green list statuses of addresses +_checks that a given `user` is not sanctioned_ -### E_USD_GREENLISTED_ROLE +### __WithSanctionsList_init ```solidity -bytes32 E_USD_GREENLISTED_ROLE +function __WithSanctionsList_init(address _accesControl, address _sanctionsList) internal ``` -actor that is greenlisted in eUSD - -## EUsdRedemptionVault - -Smart contract that handles eUSD redeeming +_upgradeable pattern contract`s initializer_ -### vaultRole +### __WithSanctionsList_init_unchained ```solidity -function vaultRole() public pure returns (bytes32) +function __WithSanctionsList_init_unchained(address _sanctionsList) internal ``` -### greenlistedRole +_upgradeable pattern contract`s initializer unchained_ + +### setSanctionsList ```solidity -function greenlistedRole() public pure returns (bytes32) +function setSanctionsList(address newSanctionsList) external ``` -AC role of a greenlist +updates `sanctionsList` address. +can be called only from permissioned actor that have +`sanctionsListAdminRole()` role -#### Return Values +#### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | bytes32 | role bytes32 role | - -## EUsdRedemptionVaultWithBUIDL - -Smart contract that handles eUSD redeeming +| newSanctionsList | address | new sanctions list address | -### vaultRole +### _validateSanctionListAdminAccess ```solidity -function vaultRole() public pure returns (bytes32) +function _validateSanctionListAdminAccess(address account) internal view virtual ``` -### greenlistedRole - -```solidity -function greenlistedRole() public pure returns (bytes32) -``` +_validates that the caller has access to sanctions list functions_ -AC role of a greenlist +## Blacklistable -#### Return Values +Base contract that implements basic functions and modifiers +to work with blacklistable -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | bytes32 | role bytes32 role | +### onlyNotBlacklisted -## HBUsdtMidasAccessControlRoles +```solidity +modifier onlyNotBlacklisted(address account) +``` -Base contract that stores all roles descriptors for hbUSDT contracts +_checks that a given `account` doesnt +have BLACKLISTED_ROLE_ -### HB_USDT_DEPOSIT_VAULT_ADMIN_ROLE +### __Blacklistable_init ```solidity -bytes32 HB_USDT_DEPOSIT_VAULT_ADMIN_ROLE +function __Blacklistable_init(address _accessControl) internal ``` -actor that can manage HBUsdtDepositVault +_upgradeable pattern contract`s initializer_ -### HB_USDT_REDEMPTION_VAULT_ADMIN_ROLE +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _accessControl | address | MidasAccessControl contract address | + +### __Blacklistable_init_unchained ```solidity -bytes32 HB_USDT_REDEMPTION_VAULT_ADMIN_ROLE +function __Blacklistable_init_unchained() internal ``` -actor that can manage HBUsdtRedemptionVault +_upgradeable pattern contract`s initializer unchained_ -### HB_USDT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +### _onlyNotBlacklisted ```solidity -bytes32 HB_USDT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +function _onlyNotBlacklisted(address account) internal view ``` -actor that can manage HBUsdtCustomAggregatorFeed and HBUsdtDataFeed +_checks that a given `account` doesnt +have BLACKLISTED_ROLE_ -## HBUsdtRedemptionVaultWithSwapper +## Greenlistable -Smart contract that handles hbUSDT redemptions +Base contract that implements basic functions and modifiers +to work with greenlistable -### vaultRole +### greenlistEnabled ```solidity -function vaultRole() public pure returns (bytes32) +bool greenlistEnabled ``` -## HBXautMidasAccessControlRoles - -Base contract that stores all roles descriptors for hbXAUt contracts +is greenlist enabled -### HB_XAUT_DEPOSIT_VAULT_ADMIN_ROLE +### SetGreenlistEnable ```solidity -bytes32 HB_XAUT_DEPOSIT_VAULT_ADMIN_ROLE +event SetGreenlistEnable(address sender, bool enable) ``` -actor that can manage HBXautDepositVault - -### HB_XAUT_REDEMPTION_VAULT_ADMIN_ROLE +### onlyGreenlisted ```solidity -bytes32 HB_XAUT_REDEMPTION_VAULT_ADMIN_ROLE +modifier onlyGreenlisted(address account) ``` -actor that can manage HBXautRedemptionVault +_checks that a given `account` +have `greenlistedRole()`_ -### HB_XAUT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +### __Greenlistable_init ```solidity -bytes32 HB_XAUT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +function __Greenlistable_init(address _accessControl) internal ``` -actor that can manage HBXautCustomAggregatorFeed and HBXautDataFeed +_upgradeable pattern contract`s initializer_ -## HBXautRedemptionVaultWithSwapper +#### Parameters -Smart contract that handles hbXAUt redemptions +| Name | Type | Description | +| ---- | ---- | ----------- | +| _accessControl | address | MidasAccessControl contract address | -### vaultRole +### __Greenlistable_init_unchained ```solidity -function vaultRole() public pure returns (bytes32) +function __Greenlistable_init_unchained() internal ``` -## HypeBtcMidasAccessControlRoles - -Base contract that stores all roles descriptors for hypeBTC contracts +_upgradeable pattern contract`s initializer unchained_ -### HYPE_BTC_DEPOSIT_VAULT_ADMIN_ROLE +### setGreenlistEnable ```solidity -bytes32 HYPE_BTC_DEPOSIT_VAULT_ADMIN_ROLE +function setGreenlistEnable(bool enable) external ``` -actor that can manage HypeBtcDepositVault - -### HYPE_BTC_REDEMPTION_VAULT_ADMIN_ROLE +enable or disable greenlist. +can be called only from permissioned actor. -```solidity -bytes32 HYPE_BTC_REDEMPTION_VAULT_ADMIN_ROLE -``` +#### Parameters -actor that can manage HypeBtcRedemptionVault +| Name | Type | Description | +| ---- | ---- | ----------- | +| enable | bool | enable | -### HYPE_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +### greenlistedRole ```solidity -bytes32 HYPE_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +function greenlistedRole() public view virtual returns (bytes32) ``` -actor that can manage HypeBtcCustomAggregatorFeed and HypeBtcDataFeed +AC role of a greenlist -## HypeBtcRedemptionVaultWithSwapper +#### Return Values -Smart contract that handles hypeBTC redemptions +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role bytes32 role | -### vaultRole +### _validateGreenlistableAdminAccess ```solidity -function vaultRole() public pure returns (bytes32) +function _validateGreenlistableAdminAccess(address account) internal view virtual ``` -## HypeEthMidasAccessControlRoles +_checks that a given `account` has access to greenlistable functions_ -Base contract that stores all roles descriptors for hypeETH contracts +## MidasAccessControl -### HYPE_ETH_DEPOSIT_VAULT_ADMIN_ROLE +Smart contract that stores all roles for Midas project + +### functionAccessAdminRoleEnabled ```solidity -bytes32 HYPE_ETH_DEPOSIT_VAULT_ADMIN_ROLE +mapping(bytes32 => bool) functionAccessAdminRoleEnabled ``` -actor that can manage HypeEthDepositVault +_Only when true may holders of `masterRole` manage grant operators for that role's scopes._ -### HYPE_ETH_REDEMPTION_VAULT_ADMIN_ROLE +### initialize ```solidity -bytes32 HYPE_ETH_REDEMPTION_VAULT_ADMIN_ROLE +function initialize() external ``` -actor that can manage HypeEthRedemptionVault +upgradeable pattern contract`s initializer -### HYPE_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +### setUserFacingRoleMult ```solidity -bytes32 HYPE_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +function setUserFacingRoleMult(struct IMidasAccessControl.SetUserFacingRoleParams[] params) external ``` -actor that can manage HypeEthCustomAggregatorFeed and HypeEthDataFeed +Enable or disable which OZ role may administer function-access scopes for that role. -## HypeEthRedemptionVaultWithSwapper +_Only `DEFAULT_ADMIN_ROLE` can call this function. +Prevents unrelated role admins from spamming access mappings._ -Smart contract that handles hypeETH redemptions +#### Parameters -### vaultRole +| Name | Type | Description | +| ---- | ---- | ----------- | +| params | struct IMidasAccessControl.SetUserFacingRoleParams[] | array of SetUserFacingRoleParams | + +### setGrantOperatorRoleMult ```solidity -function vaultRole() public pure returns (bytes32) +function setGrantOperatorRoleMult(struct IMidasAccessControl.SetGrantOperatorRoleParams[] params) external ``` -## HypeUsdMidasAccessControlRoles +Add or remove a grant operator for a specific contract function scope. -Base contract that stores all roles descriptors for hypeUSD contracts +_Caller must hold `masterRole`; role must be enabled via `setFunctionAccessAdminRoleEnabled`._ -### HYPE_USD_DEPOSIT_VAULT_ADMIN_ROLE +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| params | struct IMidasAccessControl.SetGrantOperatorRoleParams[] | array of SetGrantOperatorRoleParams | + +### setPermissionRoleMult ```solidity -bytes32 HYPE_USD_DEPOSIT_VAULT_ADMIN_ROLE +function setPermissionRoleMult(struct IMidasAccessControl.SetPermissionRoleParams[] params) external ``` -actor that can manage HypeUsdDepositVault +Grant or revoke function access for an account -### HYPE_USD_REDEMPTION_VAULT_ADMIN_ROLE +_caller must be a grant operator for the scope_ -```solidity -bytes32 HYPE_USD_REDEMPTION_VAULT_ADMIN_ROLE -``` +#### Parameters -actor that can manage HypeUsdRedemptionVault +| Name | Type | Description | +| ---- | ---- | ----------- | +| params | struct IMidasAccessControl.SetPermissionRoleParams[] | array of SetPermissionRoleParams | -### HYPE_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +### grantRoleMult ```solidity -bytes32 HYPE_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +function grantRoleMult(bytes32[] roles, address[] addresses) external ``` -actor that can manage HypeUsdCustomAggregatorFeed and HypeUsdDataFeed - -## HypeUsdRedemptionVaultWithSwapper - -Smart contract that handles hypeUSD redemptions +grant multiple roles to multiple users +in one transaction -### vaultRole +_length`s of 2 arays should match_ -```solidity -function vaultRole() public pure returns (bytes32) -``` +#### Parameters -## IDataFeed +| Name | Type | Description | +| ---- | ---- | ----------- | +| roles | bytes32[] | array of bytes32 roles | +| addresses | address[] | array of user addresses | -### initialize +### revokeRoleMult ```solidity -function initialize(address _ac, address _aggregator, uint256 _healthyDiff, int256 _minExpectedAnswer, int256 _maxExpectedAnswer) external +function revokeRoleMult(bytes32[] roles, address[] addresses) external ``` -upgradeable pattern contract`s initializer +revoke multiple roles from multiple users +in one transaction + +_length`s of 2 arays should match_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| _ac | address | MidasAccessControl contract address | -| _aggregator | address | AggregatorV3Interface contract address | -| _healthyDiff | uint256 | max. staleness time for data feed answers | -| _minExpectedAnswer | int256 | min.expected answer value from data feed | -| _maxExpectedAnswer | int256 | max.expected answer value from data feed | +| roles | bytes32[] | array of bytes32 roles | +| addresses | address[] | array of user addresses | -### changeAggregator +### setRoleAdmin ```solidity -function changeAggregator(address _aggregator) external +function setRoleAdmin(bytes32 role, bytes32 newAdminRole) external ``` -updates `aggregator` address +set the admin role for a specific role + +_can be called only by the address that holds `DEFAULT_ADMIN_ROLE`_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| _aggregator | address | new AggregatorV3Interface contract address | +| role | bytes32 | the role to set the admin role for | +| newAdminRole | bytes32 | the new admin role | -### getDataInBase18 +### renounceRole ```solidity -function getDataInBase18() external view returns (uint256 answer) +function renounceRole(bytes32, address) public pure ``` -fetches answer from aggregator -and converts it to the base18 precision - -#### Return Values - -| Name | Type | Description | -| ---- | ---- | ----------- | -| answer | uint256 | fetched aggregator answer | - -### feedAdminRole +### isFunctionAccessGrantOperator ```solidity -function feedAdminRole() external view returns (bytes32) +function isFunctionAccessGrantOperator(bytes32 masterRole, address targetContract, bytes4 functionSelector, address operator) external view returns (bool) ``` -_describes a role, owner of which can manage this feed_ +Whether `operator` may call `setFunctionPermission` for the function scope -#### Return Values +#### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | bytes32 | role descriptor | - -## IMTbill - -### mint - -```solidity -function mint(address to, uint256 amount) external -``` - -mints mTBILL token `amount` to a given `to` address. -should be called only from permissioned actor +| masterRole | bytes32 | OZ role for the scope | +| targetContract | address | scoped contract | +| functionSelector | bytes4 | scoped function | +| operator | address | address checked for grant-operator status | -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| to | address | addres to mint tokens to | -| amount | uint256 | amount to mint | +| [0] | bool | allowed whether `operator` is a grant operator for the scope | -### burn +### hasFunctionPermission ```solidity -function burn(address from, uint256 amount) external +function hasFunctionPermission(bytes32 masterRole, address targetContract, bytes4 functionSelector, address account) external view returns (bool) ``` -burns mTBILL token `amount` to a given `to` address. -should be called only from permissioned actor +Whether `account` may call the scoped function on `targetContract`. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| from | address | addres to burn tokens from | -| amount | uint256 | amount to burn | - -### setMetadata +| masterRole | bytes32 | OZ role for the scope | +| targetContract | address | scoped contract | +| functionSelector | bytes4 | scoped function | +| account | address | address checked for permissio. | -```solidity -function setMetadata(bytes32 key, bytes data) external -``` - -updates contract`s metadata. -should be called only from permissioned actor - -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| key | bytes32 | metadata map. key | -| data | bytes | metadata map. value | +| [0] | bool | allowed whether `account` has function access for the scope | -### pause +## MidasAccessControlRoles + +Base contract that stores all roles descriptors + +### GREENLIST_OPERATOR_ROLE ```solidity -function pause() external +bytes32 GREENLIST_OPERATOR_ROLE ``` -puts mTBILL token on pause. -should be called only from permissioned actor +actor that can change green list statuses of addresses -### unpause +_keccak256("GREENLIST_OPERATOR_ROLE")_ + +### BLACKLIST_OPERATOR_ROLE ```solidity -function unpause() external +bytes32 BLACKLIST_OPERATOR_ROLE ``` -puts mTBILL token on pause. -should be called only from permissioned actor +actor that can change black list statuses of addresses -## TokenConfig +_keccak256("BLACKLIST_OPERATOR_ROLE")_ + +### GREENLISTED_ROLE ```solidity -struct TokenConfig { - address dataFeed; - uint256 fee; - uint256 allowance; - bool stable; -} +bytes32 GREENLISTED_ROLE ``` -## RequestStatus +actor that is greenlisted -```solidity -enum RequestStatus { - Pending, - Processed, - Canceled -} -``` +_keccak256("GREENLISTED_ROLE")_ -## MTokenInitParams +### BLACKLISTED_ROLE ```solidity -struct MTokenInitParams { - address mToken; - address mTokenDataFeed; -} +bytes32 BLACKLISTED_ROLE ``` -## ReceiversInitParams +actor that is blacklisted -```solidity -struct ReceiversInitParams { - address tokensReceiver; - address feeReceiver; -} -``` +_keccak256("BLACKLISTED_ROLE")_ + +## Pausable -## InstantInitParams +Base contract that implements basic functions and modifiers +with pause functionality + +### fnPaused ```solidity -struct InstantInitParams { - uint256 instantFee; - uint256 instantDailyLimit; -} +mapping(bytes4 => bool) fnPaused ``` -## IManageableVault +function id => paused status -### WithdrawToken +### PauseFn ```solidity -event WithdrawToken(address caller, address token, address withdrawTo, uint256 amount) +event PauseFn(address caller, bytes4 fn) ``` #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| caller | address | function caller (msg.sender) | -| token | address | token that was withdrawn | -| withdrawTo | address | address to which tokens were withdrawn | -| amount | uint256 | `token` transfer amount | +| caller | address | caller address (msg.sender) | +| fn | bytes4 | function id | -### AddPaymentToken +### UnpauseFn ```solidity -event AddPaymentToken(address caller, address token, address dataFeed, uint256 fee, bool stable) +event UnpauseFn(address caller, bytes4 fn) ``` #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| caller | address | function caller (msg.sender) | -| token | address | address of token that | -| dataFeed | address | token dataFeed address | -| fee | uint256 | fee 1% = 100 | -| stable | bool | stablecoin flag | +| caller | address | caller address (msg.sender) | +| fn | bytes4 | function id | -### ChangeTokenAllowance +### onlyPauseAdmin ```solidity -event ChangeTokenAllowance(address token, address caller, uint256 allowance) +modifier onlyPauseAdmin() ``` -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| token | address | address of token that | -| caller | address | function caller (msg.sender) | -| allowance | uint256 | new allowance | +_checks that a given `account` has access to pause functions_ -### ChangeTokenFee +### __Pausable_init ```solidity -event ChangeTokenFee(address token, address caller, uint256 fee) +function __Pausable_init(address _accessControl) internal ``` +_upgradeable pattern contract`s initializer_ + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| token | address | address of token that | -| caller | address | function caller (msg.sender) | -| fee | uint256 | new fee | +| _accessControl | address | MidasAccessControl contract address | -### RemovePaymentToken +### pause ```solidity -event RemovePaymentToken(address token, address caller) +function pause() external ``` -#### Parameters +### unpause -| Name | Type | Description | -| ---- | ---- | ----------- | -| token | address | address of token that | -| caller | address | function caller (msg.sender) | +```solidity +function unpause() external +``` -### AddWaivedFeeAccount +### pauseFn ```solidity -event AddWaivedFeeAccount(address account, address caller) +function pauseFn(bytes4 fn) external ``` +_pause specific function_ + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| account | address | address of account | -| caller | address | function caller (msg.sender) | +| fn | bytes4 | function id | -### RemoveWaivedFeeAccount +### unpauseFn ```solidity -event RemoveWaivedFeeAccount(address account, address caller) +function unpauseFn(bytes4 fn) external ``` +_unpause specific function_ + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| account | address | address of account | -| caller | address | function caller (msg.sender) | +| fn | bytes4 | function id | -### SetInstantFee +### _validatePauseAdminAccess ```solidity -event SetInstantFee(address caller, uint256 newFee) +function _validatePauseAdminAccess(address account) internal view virtual ``` +_validates that the caller has access to pause functions_ + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| caller | address | function caller (msg.sender) | -| newFee | uint256 | new operation fee value | +| account | address | account address | -### SetMinAmount +### _requireFnNotPaused ```solidity -event SetMinAmount(address caller, uint256 newAmount) +function _requireFnNotPaused(bytes4 fn, bool validateGlobalPause) internal view ``` +_checks that a given `fn` is not paused_ + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| caller | address | function caller (msg.sender) | -| newAmount | uint256 | new min amount for operation | +| fn | bytes4 | function id | +| validateGlobalPause | bool | if true, validates if global pause is not paused | + +## WithMidasAccessControl + +Base contract that consumes MidasAccessControl -### SetInstantDailyLimit +### DEFAULT_ADMIN_ROLE ```solidity -event SetInstantDailyLimit(address caller, uint256 newLimit) +bytes32 DEFAULT_ADMIN_ROLE ``` -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| caller | address | function caller (msg.sender) | -| newLimit | uint256 | new operation daily limit | +admin role -### SetVariationTolerance +### accessControl ```solidity -event SetVariationTolerance(address caller, uint256 newTolerance) +contract MidasAccessControl accessControl ``` -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| caller | address | function caller (msg.sender) | -| newTolerance | uint256 | percent of price diviation 1% = 100 | +MidasAccessControl contract address -### SetFeeReceiver +### onlyRole ```solidity -event SetFeeReceiver(address caller, address reciever) +modifier onlyRole(bytes32 role, address account) ``` -#### Parameters +_checks that given `address` have `role`_ -| Name | Type | Description | -| ---- | ---- | ----------- | -| caller | address | function caller (msg.sender) | -| reciever | address | new reciever address | +### onlyNotRole -### SetTokensReceiver +```solidity +modifier onlyNotRole(bytes32 role, address account) +``` + +_checks that given `address` do not have `role`_ + +### __WithMidasAccessControl_init ```solidity -event SetTokensReceiver(address caller, address reciever) +function __WithMidasAccessControl_init(address _accessControl) internal ``` -#### Parameters +_upgradeable pattern contract`s initializer_ -| Name | Type | Description | -| ---- | ---- | ----------- | -| caller | address | function caller (msg.sender) | -| reciever | address | new reciever address | +### _onlyRole -### FreeFromMinAmount +```solidity +function _onlyRole(bytes32 role, address account) internal view +``` + +_checks that given `address` have `role`_ + +### _onlyNotRole ```solidity -event FreeFromMinAmount(address user, bool enable) +function _onlyNotRole(bytes32 role, address account) internal view ``` +_checks that given `address` do not have `role`_ + +### _hasFunctionPermission + +```solidity +function _hasFunctionPermission(bytes32 masterRole, bytes4 functionSelector, address account) internal view +``` + +_checks that given `account` has function permission for the given function selector_ + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| user | address | user address | -| enable | bool | is enabled | +| masterRole | bytes32 | OZ role for the scope | +| functionSelector | bytes4 | function selector | +| account | address | address checked for permission | -### mTokenDataFeed +## IDataFeed + +### getDataInBase18 ```solidity -function mTokenDataFeed() external view returns (contract IDataFeed) +function getDataInBase18() external view returns (uint256 answer) ``` -The mTokenDataFeed contract address. +fetches answer from aggregator +and converts it to the base18 precision #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | contract IDataFeed | The address of the mTokenDataFeed contract. | +| answer | uint256 | fetched aggregator answer | -### mToken +### feedAdminRole ```solidity -function mToken() external view returns (contract IMTbill) +function feedAdminRole() external view returns (bytes32) ``` -The mToken contract address. +_describes a role, owner of which can manage this feed_ #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | contract IMTbill | The address of the mToken contract. | +| [0] | bytes32 | role descriptor | -### withdrawToken +## Request + +Legacy Mint request scruct + +_used for backward compatibility_ + +```solidity +struct Request { + address sender; + address tokenIn; + enum RequestStatus status; + uint256 depositedUsdAmount; + uint256 usdAmountWithoutFees; + uint256 tokenOutRate; +} +``` + +## RequestV2 + +Mint request scruct + +_replaces `Request` struct and adds next fields: +- `depositedInstantUsdAmount` +- `approvedMTokenRate` +- `version`_ ```solidity -function withdrawToken(address token, uint256 amount, address withdrawTo) external +struct RequestV2 { + address sender; + address tokenIn; + enum RequestStatus status; + uint256 depositedUsdAmount; + uint256 usdAmountWithoutFees; + uint256 tokenOutRate; + uint256 depositedInstantUsdAmount; + uint256 approvedTokenOutRate; + uint8 version; +} ``` -withdraws `amount` of a given `token` from the contract. -can be called only from permissioned actor. +## IDepositVault + +### SetMinMTokenAmountForFirstDeposit + +```solidity +event SetMinMTokenAmountForFirstDeposit(address caller, uint256 newValue) +``` #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| token | address | token address | -| amount | uint256 | token amount | -| withdrawTo | address | withdraw destination address | +| caller | address | function caller (msg.sender) | +| newValue | uint256 | new min amount to deposit value | -### addPaymentToken +### SetMaxSupplyCap ```solidity -function addPaymentToken(address token, address dataFeed, uint256 fee, bool stable) external +event SetMaxSupplyCap(address caller, uint256 newValue) ``` -adds a token to the stablecoins list. -can be called only from permissioned actor. - #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| token | address | token address | -| dataFeed | address | dataFeed address | -| fee | uint256 | 1% = 100 | -| stable | bool | is stablecoin flag | +| caller | address | function caller (msg.sender) | +| newValue | uint256 | new max supply cap value | -### removePaymentToken +### DepositInstant ```solidity -function removePaymentToken(address token) external +event DepositInstant(address user, address tokenIn, address recipient, uint256 amountUsd, uint256 amountToken, uint256 fee, uint256 minted, bytes32 referrerId) ``` -removes a token from stablecoins list. -can be called only from permissioned actor. - #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| token | address | token address | +| user | address | function caller (msg.sender) | +| tokenIn | address | address of tokenIn | +| recipient | address | address that receives the mTokens | +| amountUsd | uint256 | amount of tokenIn converted to USD | +| amountToken | uint256 | amount of tokenIn | +| fee | uint256 | fee amount in tokenIn | +| minted | uint256 | amount of minted mTokens | +| referrerId | bytes32 | referrer id | -### changeTokenAllowance +### DepositRequest ```solidity -function changeTokenAllowance(address token, uint256 allowance) external +event DepositRequest(uint256 requestId, address user, address tokenIn, address recipient, uint256 amountToken, uint256 amountUsd, uint256 fee, uint256 tokenOutRate, bytes32 referrerId) ``` -set new token allowance. -if MAX_UINT = infinite allowance -prev allowance rewrites by new -can be called only from permissioned actor. - #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| token | address | token address | -| allowance | uint256 | new allowance (decimals 18) | +| requestId | uint256 | mint request id | +| user | address | function caller (msg.sender) | +| tokenIn | address | address of tokenIn | +| recipient | address | address that receives the mTokens | +| amountToken | uint256 | amount of tokenIn | +| amountUsd | uint256 | amount of tokenIn converted to USD | +| fee | uint256 | fee amount in tokenIn | +| tokenOutRate | uint256 | mToken rate | +| referrerId | bytes32 | referrer id | -### changeTokenFee +### ApproveRequest ```solidity -function changeTokenFee(address token, uint256 fee) external +event ApproveRequest(uint256 requestId, uint256 newOutRate, bool isSafe, bool isAvgRate) ``` -set new token fee. -can be called only from permissioned actor. - #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| token | address | token address | -| fee | uint256 | new fee percent 1% = 100 | +| requestId | uint256 | mint request id | +| newOutRate | uint256 | mToken rate inputted by admin | +| isSafe | bool | if true, approval is safe | +| isAvgRate | bool | if true, newOutRate is avg rate | -### setVariationTolerance +### RejectRequest ```solidity -function setVariationTolerance(uint256 tolerance) external +event RejectRequest(uint256 requestId, address user) ``` -set new prices diviation percent. -can be called only from permissioned actor. +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | mint request id | +| user | address | address of user | + +### FreeFromMinDeposit + +```solidity +event FreeFromMinDeposit(address user) +``` #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tolerance | uint256 | new prices diviation percent 1% = 100 | +| user | address | address that was freed from min deposit check | -### setMinAmount +### depositInstant ```solidity -function setMinAmount(uint256 newAmount) external +function depositInstant(address tokenIn, uint256 amountToken, uint256 minReceiveAmount, bytes32 referrerId) external ``` -set new min amount. -can be called only from permissioned actor. +depositing proccess with auto mint if +account fit daily limit and token allowance. +Transfers token from the user. +Transfers fee in tokenIn to feeReceiver. +Mints mToken to user. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| newAmount | uint256 | min amount for operations in mToken | +| tokenIn | address | address of tokenIn | +| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | +| minReceiveAmount | uint256 | minimum expected amount of mToken to receive (decimals 18) | +| referrerId | bytes32 | referrer id | -### addWaivedFeeAccount +### depositInstant ```solidity -function addWaivedFeeAccount(address account) external +function depositInstant(address tokenIn, uint256 amountToken, uint256 minReceiveAmount, bytes32 referrerId, address tokensReceiver) external ``` -adds a account to waived fee restriction. -can be called only from permissioned actor. +Does the same as original `depositInstant` but allows specifying a custom tokensReceiver address. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| account | address | user address | +| tokenIn | address | address of tokenIn | +| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | +| minReceiveAmount | uint256 | minimum expected amount of mToken to receive (decimals 18) | +| referrerId | bytes32 | referrer id | +| tokensReceiver | address | address to receive the tokens (instead of msg.sender) | -### removeWaivedFeeAccount +### depositRequest ```solidity -function removeWaivedFeeAccount(address account) external +function depositRequest(address tokenIn, uint256 amountToken, bytes32 referrerId) external returns (uint256) ``` -removes a account from waived fee restriction. -can be called only from permissioned actor. +depositing proccess with mint request creating if +account fit token allowance. +Transfers token from the user. +Transfers fee in tokenIn to feeReceiver. +Creates mint request. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| account | address | user address | +| tokenIn | address | address of tokenIn | +| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | +| referrerId | bytes32 | referrer id | -### setFeeReceiver +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | request id | + +### depositRequest ```solidity -function setFeeReceiver(address reciever) external +function depositRequest(address tokenIn, uint256 amountToken, bytes32 referrerId, address recipient) external returns (uint256) ``` -set new reciever for fees. -can be called only from permissioned actor. +Does the same as original `depositRequest` but allows specifying a custom tokensReceiver address. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| reciever | address | new fee reciever address | +| tokenIn | address | address of tokenIn | +| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | +| referrerId | bytes32 | referrer id | +| recipient | address | address that receives the mTokens | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | request id | -### setTokensReceiver +### depositRequest ```solidity -function setTokensReceiver(address reciever) external +function depositRequest(address tokenIn, uint256 amountToken, bytes32 referrerId, address recipientRequest, uint256 instantShare, uint256 minReceiveAmountInstantShare, address recipientInstant) external returns (uint256) ``` -set new reciever for tokens. -can be called only from permissioned actor. +Instantly deposits `instantShare` amount of `amountMTokenIn` and creates a request for the remaining amount. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| reciever | address | new token reciever address | +| tokenIn | address | address of tokenIn | +| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | +| referrerId | bytes32 | referrer id | +| recipientRequest | address | address that receives the mTokens for the request part | +| instantShare | uint256 | % amount of `amountToken` that will be deposited instantly | +| minReceiveAmountInstantShare | uint256 | min receive amount for the instant share | +| recipientInstant | address | address that receives the mTokens for the instant part | -### setInstantFee +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | request id | + +### safeBulkApproveRequestAtSavedRate ```solidity -function setInstantFee(uint256 newInstantFee) external +function safeBulkApproveRequestAtSavedRate(uint256[] requestIds) external ``` -set operation fee percent. -can be called only from permissioned actor. +approving requests from the `requestIds` array +with the mToken rate from the request. +Does same validation as `safeApproveRequest`. +Mints mToken to request users. +Sets request flags to Processed. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| newInstantFee | uint256 | new instant operations fee percent 1& = 100 | +| requestIds | uint256[] | request ids array | -### setInstantDailyLimit +### safeBulkApproveRequest ```solidity -function setInstantDailyLimit(uint256 newInstantDailyLimit) external +function safeBulkApproveRequest(uint256[] requestIds) external ``` -set operation daily limit. -can be called only from permissioned actor. +approving requests from the `requestIds` array +with the current mToken rate. +Does same validation as `safeApproveRequest`. +Mints mToken to request users. +Sets request flags to Processed. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| newInstantDailyLimit | uint256 | new operation daily limit (decimals 18) | +| requestIds | uint256[] | request ids array | -### freeFromMinAmount +### safeBulkApproveRequestAvgRate ```solidity -function freeFromMinAmount(address user, bool enable) external +function safeBulkApproveRequestAvgRate(uint256[] requestIds) external ``` -frees given `user` from the minimal deposit -amount validation in `initiateDepositRequest` +approving requests from the `requestIds` array +with the current mToken rate. +Does same validation as `safeApproveRequestAvgRate`. +Mints mToken to request users. +Sets request flags to Processed. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| user | address | address of user | -| enable | bool | | +| requestIds | uint256[] | request ids array | -## Request +### safeBulkApproveRequest ```solidity -struct Request { - address sender; - address tokenOut; - enum RequestStatus status; - uint256 amountMToken; - uint256 mTokenRate; - uint256 tokenOutRate; -} -``` - -## FiatRedeptionInitParams - -```solidity -struct FiatRedeptionInitParams { - uint256 fiatAdditionalFee; - uint256 fiatFlatFee; - uint256 minFiatRedeemAmount; -} +function safeBulkApproveRequest(uint256[] requestIds, uint256 newOutRate) external ``` -## IRedemptionVault - -### RedeemInstant - -```solidity -event RedeemInstant(address user, address tokenOut, uint256 amount, uint256 feeAmount, uint256 amountTokenOut) -``` +approving requests from the `requestIds` array using the `newOutRate`. +Does same validation as `safeApproveRequest`. +Mints mToken to request users. +Sets request flags to Processed. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| user | address | function caller (msg.sender) | -| tokenOut | address | address of tokenOut | -| amount | uint256 | amount of mToken | -| feeAmount | uint256 | fee amount in mToken | -| amountTokenOut | uint256 | amount of tokenOut | +| requestIds | uint256[] | request ids array | +| newOutRate | uint256 | new mToken rate inputted by vault admin | -### RedeemInstantWithCustomRecipient +### safeBulkApproveRequestAvgRate ```solidity -event RedeemInstantWithCustomRecipient(address user, address tokenOut, address recipient, uint256 amount, uint256 feeAmount, uint256 amountTokenOut) +function safeBulkApproveRequestAvgRate(uint256[] requestIds, uint256 avgMTokenRate) external ``` +approving requests from the `requestIds` array using the `newOutRate`. +Does same validation as `safeApproveRequestAvgRate`. +Mints mToken to request users. +Sets request flags to Processed. + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| user | address | function caller (msg.sender) | -| tokenOut | address | address of tokenOut | -| recipient | address | address that receives tokens | -| amount | uint256 | amount of mToken | -| feeAmount | uint256 | fee amount in mToken | -| amountTokenOut | uint256 | amount of tokenOut | +| requestIds | uint256[] | request ids array | +| avgMTokenRate | uint256 | avg mToken rate inputted by vault admin | -### RedeemRequest +### safeApproveRequest ```solidity -event RedeemRequest(uint256 requestId, address user, address tokenOut, uint256 amountMTokenIn, uint256 feeAmount) +function safeApproveRequest(uint256 requestId, uint256 newOutRate) external ``` +approving request if inputted token rate fit price deviation percent +Mints mToken to user. +Sets request flag to Processed. + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | | requestId | uint256 | request id | -| user | address | function caller (msg.sender) | -| tokenOut | address | address of tokenOut | -| amountMTokenIn | uint256 | amount of mToken | -| feeAmount | uint256 | fee amount in mToken | +| newOutRate | uint256 | mToken rate inputted by vault admin | -### RedeemRequestWithCustomRecipient +### safeApproveRequestAvgRate ```solidity -event RedeemRequestWithCustomRecipient(uint256 requestId, address user, address tokenOut, address recipient, uint256 amountMTokenIn, uint256 feeAmount) +function safeApproveRequestAvgRate(uint256 requestId, uint256 avgMTokenRate) external ``` +approving request if inputted token rate fit price deviation percent +Mints mToken to user. +Sets request flag to Processed. + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | | requestId | uint256 | request id | -| user | address | function caller (msg.sender) | -| tokenOut | address | address of tokenOut | -| recipient | address | address that receives tokens | -| amountMTokenIn | uint256 | amount of mToken | -| feeAmount | uint256 | fee amount in mToken | +| avgMTokenRate | uint256 | avg mToken rate inputted by vault admin | -### ApproveRequest +### approveRequest ```solidity -event ApproveRequest(uint256 requestId, uint256 newMTokenRate) +function approveRequest(uint256 requestId, uint256 newOutRate) external ``` +approving request without price deviation check +Mints mToken to user. +Sets request flag to Processed. + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| requestId | uint256 | mint request id | -| newMTokenRate | uint256 | net mToken rate | +| requestId | uint256 | request id | +| newOutRate | uint256 | mToken rate inputted by vault admin | -### SafeApproveRequest +### approveRequestAvgRate ```solidity -event SafeApproveRequest(uint256 requestId, uint256 newMTokenRate) +function approveRequestAvgRate(uint256 requestId, uint256 avgMTokenRate) external ``` +approving request without price deviation check +Mints mToken to user. +Sets request flag to Processed. + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| requestId | uint256 | mint request id | -| newMTokenRate | uint256 | net mToken rate | +| requestId | uint256 | request id | +| avgMTokenRate | uint256 | avg mToken rate inputted by vault admin | -### RejectRequest +### rejectRequest ```solidity -event RejectRequest(uint256 requestId, address user) +function rejectRequest(uint256 requestId) external ``` +rejecting request +Sets request flag to Canceled. + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| requestId | uint256 | mint request id | -| user | address | address of user | +| requestId | uint256 | request id | -### SetMinFiatRedeemAmount +### setMinMTokenAmountForFirstDeposit ```solidity -event SetMinFiatRedeemAmount(address caller, uint256 newMinAmount) +function setMinMTokenAmountForFirstDeposit(uint256 newValue) external ``` +sets new minimal amount to deposit in EUR. +can be called only from vault`s admin + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| caller | address | function caller (msg.sender) | -| newMinAmount | uint256 | new min amount for fiat requests | +| newValue | uint256 | new min. deposit value | -### SetFiatFlatFee +### setMaxSupplyCap ```solidity -event SetFiatFlatFee(address caller, uint256 feeInMToken) +function setMaxSupplyCap(uint256 newValue) external ``` +sets new max supply cap value +can be called only from vault`s admin + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| caller | address | function caller (msg.sender) | -| feeInMToken | uint256 | fee amount in mToken | +| newValue | uint256 | new max supply cap value | + +## IMToken -### SetFiatAdditionalFee +### mint ```solidity -event SetFiatAdditionalFee(address caller, uint256 newfee) +function mint(address to, uint256 amount) external ``` +mints mToken token `amount` to a given `to` address. +should be called only from permissioned actor + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| caller | address | function caller (msg.sender) | -| newfee | uint256 | new fiat fee percent 1% = 100 | +| to | address | addres to mint tokens to | +| amount | uint256 | amount to mint | -### SetRequestRedeemer +### burn ```solidity -event SetRequestRedeemer(address caller, address redeemer) +function burn(address from, uint256 amount) external ``` +burns mToken token `amount` to a given `to` address. +should be called only from permissioned actor + #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| caller | address | function caller (msg.sender) | -| redeemer | address | new address of request redeemer | +| from | address | addres to burn tokens from | +| amount | uint256 | amount to burn | -### redeemInstant +### setMetadata ```solidity -function redeemInstant(address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount) external +function setMetadata(bytes32 key, bytes data) external ``` -redeem mToken to tokenOut if daily limit and allowance not exceeded -Burns mTBILL from the user. -Transfers fee in mToken to feeReceiver -Transfers tokenOut to user. +updates contract`s metadata. +should be called only from permissioned actor #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tokenOut | address | stable coin token address to redeem to | -| amountMTokenIn | uint256 | amount of mTBILL to redeem (decimals 18) | -| minReceiveAmount | uint256 | minimum expected amount of tokenOut to receive (decimals 18) | +| key | bytes32 | metadata map. key | +| data | bytes | metadata map. value | -### redeemInstant +### pause ```solidity -function redeemInstant(address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount, address recipient) external +function pause() external ``` -Does the same as `redeemInstant` but allows specifying a custom tokensReceiver address. - -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| tokenOut | address | stable coin token address to redeem to | -| amountMTokenIn | uint256 | amount of mTBILL to redeem (decimals 18) | -| minReceiveAmount | uint256 | minimum expected amount of tokenOut to receive (decimals 18) | -| recipient | address | address that receives tokens | +puts mToken token on pause. +should be called only from permissioned actor -### redeemRequest +### unpause ```solidity -function redeemRequest(address tokenOut, uint256 amountMTokenIn) external returns (uint256) +function unpause() external ``` -creating redeem request if tokenOut not fiat -Transfers amount in mToken to contract -Transfers fee in mToken to feeReceiver - -#### Parameters +puts mToken token on pause. +should be called only from permissioned actor -| Name | Type | Description | -| ---- | ---- | ----------- | -| tokenOut | address | stable coin token address to redeem to | -| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | +## TokenConfig -#### Return Values +### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | uint256 | request id | - -### redeemRequest ```solidity -function redeemRequest(address tokenOut, uint256 amountMTokenIn, address recipient) external returns (uint256) +struct TokenConfig { + address dataFeed; + uint256 fee; + uint256 allowance; + bool stable; +} ``` -Does the same as `redeemRequest` but allows specifying a custom tokensReceiver address. +## LimitConfig -#### Parameters +Rate limit configuration + +### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| tokenOut | address | stable coin token address to redeem to | -| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | -| recipient | address | address that receives tokens | -#### Return Values +```solidity +struct LimitConfig { + uint256 limit; + uint256 limitUsed; + uint256 lastEpoch; +} +``` -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | uint256 | request id | +## RequestStatus + +```solidity +enum RequestStatus { + Pending, + Processed, + Canceled +} +``` -### redeemFiatRequest +## CommonVaultInitParams ```solidity -function redeemFiatRequest(uint256 amountMTokenIn) external returns (uint256) +struct CommonVaultInitParams { + address ac; + address sanctionsList; + uint256 variationTolerance; + uint256 minAmount; + address mToken; + address mTokenDataFeed; + address tokensReceiver; + address feeReceiver; + uint256 instantFee; +} ``` -creating redeem request if tokenOut is fiat -Transfers amount in mToken to contract -Transfers fee in mToken to feeReceiver +## LimitConfigInitParams -#### Parameters +```solidity +struct LimitConfigInitParams { + uint256 window; + uint256 limit; +} +``` -| Name | Type | Description | -| ---- | ---- | ----------- | -| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | +## CommonVaultV2InitParams -#### Return Values +```solidity +struct CommonVaultV2InitParams { + uint64 minInstantFee; + uint64 maxInstantFee; + uint64 maxInstantShare; + struct LimitConfigInitParams[] limitConfigs; +} +``` -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | uint256 | request id | +## IManageableVault -### approveRequest +### WithdrawToken ```solidity -function approveRequest(uint256 requestId, uint256 newMTokenRate) external +event WithdrawToken(address caller, address token, address withdrawTo, uint256 amount) ``` -approving redeem request if not exceed tokenOut allowance -Burns amount mToken from contract -Transfers tokenOut to user -Sets flag Processed - #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| requestId | uint256 | request id | -| newMTokenRate | uint256 | new mToken rate inputted by vault admin | +| caller | address | function caller (msg.sender) | +| token | address | token that was withdrawn | +| withdrawTo | address | address to which tokens were withdrawn | +| amount | uint256 | `token` transfer amount | -### safeApproveRequest +### AddPaymentToken ```solidity -function safeApproveRequest(uint256 requestId, uint256 newMTokenRate) external +event AddPaymentToken(address caller, address token, address dataFeed, uint256 fee, uint256 allowance, bool stable) ``` -approving request if inputted token rate fit price diviation percent -Burns amount mToken from contract -Transfers tokenOut to user -Sets flag Processed - #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| requestId | uint256 | request id | -| newMTokenRate | uint256 | new mToken rate inputted by vault admin | +| caller | address | function caller (msg.sender) | +| token | address | address of token that | +| dataFeed | address | token dataFeed address | +| fee | uint256 | fee 1% = 100 | +| allowance | uint256 | token allowance (decimals 18) | +| stable | bool | stablecoin flag | -### rejectRequest +### ChangeTokenAllowance ```solidity -function rejectRequest(uint256 requestId) external +event ChangeTokenAllowance(address token, address caller, uint256 allowance) ``` -rejecting request -Sets request flag to Canceled. - #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| requestId | uint256 | request id | +| token | address | address of token that | +| caller | address | function caller (msg.sender) | +| allowance | uint256 | new allowance | -### setMinFiatRedeemAmount +### ChangeTokenFee ```solidity -function setMinFiatRedeemAmount(uint256 newValue) external +event ChangeTokenFee(address token, address caller, uint256 fee) ``` -set new min amount for fiat requests - #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| newValue | uint256 | new min amount | +| token | address | address of token that | +| caller | address | function caller (msg.sender) | +| fee | uint256 | new fee | -### setFiatFlatFee +### RemovePaymentToken ```solidity -function setFiatFlatFee(uint256 feeInMToken) external +event RemovePaymentToken(address token, address caller) ``` -set fee amount in mToken for fiat requests - #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| feeInMToken | uint256 | fee amount in mToken | +| token | address | address of token that | +| caller | address | function caller (msg.sender) | -### setFiatAdditionalFee +### AddWaivedFeeAccount ```solidity -function setFiatAdditionalFee(uint256 newFee) external +event AddWaivedFeeAccount(address account, address caller) ``` -set new fee percent for fiat requests - #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| newFee | uint256 | new fee percent 1% = 100 | +| account | address | address of account | +| caller | address | function caller (msg.sender) | -### setRequestRedeemer +### RemoveWaivedFeeAccount ```solidity -function setRequestRedeemer(address redeemer) external +event RemoveWaivedFeeAccount(address account, address caller) ``` -set address which is designated for standard redemptions, allowing tokens to be pulled from this address - #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| redeemer | address | new address of request redeemer | - -## IRedemptionVaultWithSwapper +| account | address | address of account | +| caller | address | function caller (msg.sender) | -### SetLiquidityProvider +### SetInstantFee ```solidity -event SetLiquidityProvider(address caller, address provider) +event SetInstantFee(address caller, uint256 newFee) ``` #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| caller | address | caller address (msg.sender) | -| provider | address | new LP address | +| caller | address | function caller (msg.sender) | +| newFee | uint256 | new operation fee value | -### SetSwapperVault +### SetMinMaxInstantFee ```solidity -event SetSwapperVault(address caller, address vault) +event SetMinMaxInstantFee(address caller, uint64 newMinInstantFee, uint64 newMaxInstantFee) ``` #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| caller | address | caller address (msg.sender) | -| vault | address | new underlying vault for swapper | +| caller | address | function caller (msg.sender) | +| newMinInstantFee | uint64 | new minimum instant fee | +| newMaxInstantFee | uint64 | new maximum instant fee | -### setLiquidityProvider +### SetMinAmount ```solidity -function setLiquidityProvider(address provider) external +event SetMinAmount(address caller, uint256 newAmount) ``` -sets new liquidity provider address - #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| provider | address | new liquidity provider address | +| caller | address | function caller (msg.sender) | +| newAmount | uint256 | new min amount for operation | -### setSwapperVault +### SetInstantLimitConfig ```solidity -function setSwapperVault(address vault) external +event SetInstantLimitConfig(address caller, uint256 window, uint256 limit) ``` -sets new underlying vault for swapper - #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| vault | address | new underlying vault for swapper | - -## ISanctionsList +| caller | address | function caller (msg.sender) | +| window | uint256 | window duration in seconds | +| limit | uint256 | limit amount per window | -### isSanctioned +### SetMaxInstantShare ```solidity -function isSanctioned(address addr) external view returns (bool) +event SetMaxInstantShare(address caller, uint64 newMaxInstantShare) ``` -## IRedemption +#### Parameters -### asset +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | function caller (msg.sender) | +| newMaxInstantShare | uint64 | new maximum instant share value in basis points (100 = 1%) | + +### RemoveInstantLimitConfig ```solidity -function asset() external view returns (address) +event RemoveInstantLimitConfig(address caller, uint256 window) ``` -The asset being redeemed. - -#### Return Values +#### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | address | The address of the asset token. | +| caller | address | function caller (msg.sender) | +| window | uint256 | window duration in seconds | -### liquidity +### SetVariationTolerance ```solidity -function liquidity() external view returns (address) +event SetVariationTolerance(address caller, uint256 newTolerance) ``` -The liquidity token that the asset is being redeemed for. - -#### Return Values +#### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | address | The address of the liquidity token. | +| caller | address | function caller (msg.sender) | +| newTolerance | uint256 | percent of price diviation 1% = 100 | -### settlement +### SetFeeReceiver ```solidity -function settlement() external view returns (address) +event SetFeeReceiver(address caller, address receiver) ``` -The settlement contract address. - -#### Return Values +#### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | address | The address of the settlement contract. | +| caller | address | function caller (msg.sender) | +| receiver | address | new receiver address | -### redeem +### SetTokensReceiver ```solidity -function redeem(uint256 amount) external +event SetTokensReceiver(address caller, address receiver) ``` -Redeems an amount of asset for liquidity - #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| amount | uint256 | The amount of the asset token to redeem | - -## IUSTBRedemption +| caller | address | function caller (msg.sender) | +| receiver | address | new receiver address | -### SUPERSTATE_TOKEN +### SetMaxApproveRequestId ```solidity -function SUPERSTATE_TOKEN() external view returns (address) +event SetMaxApproveRequestId(address caller, uint256 newMaxApproveRequestId) ``` -### USDC +#### Parameters -```solidity -function USDC() external view returns (address) -``` +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | function caller (msg.sender) | +| newMaxApproveRequestId | uint256 | new max requestId that can be approved | -### owner +### FreeFromMinAmount ```solidity -function owner() external view returns (address) +event FreeFromMinAmount(address user, bool enable) ``` -### redeem +#### Parameters -```solidity -function redeem(uint256 superstateTokenInAmount) external -``` +| Name | Type | Description | +| ---- | ---- | ----------- | +| user | address | user address | +| enable | bool | is enabled | -### setRedemptionFee +### mTokenDataFeed ```solidity -function setRedemptionFee(uint256 _newFee) external +function mTokenDataFeed() external view returns (contract IDataFeed) ``` -### calculateFee +The mTokenDataFeed contract address. -```solidity -function calculateFee(uint256 amount) external view returns (uint256) -``` +#### Return Values -### calculateUstbIn +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | contract IDataFeed | The address of the mTokenDataFeed contract. | + +### mToken ```solidity -function calculateUstbIn(uint256 usdcOutAmount) external view returns (uint256 ustbInAmount, uint256 usdPerUstbChainlinkRaw) +function mToken() external view returns (contract IMToken) ``` -## DecimalsCorrectionLibrary +The mToken contract address. -### convert +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | contract IMToken | The address of the mToken contract. | + +### addPaymentToken ```solidity -function convert(uint256 originalAmount, uint256 originalDecimals, uint256 decidedDecimals) internal pure returns (uint256) +function addPaymentToken(address token, address dataFeed, uint256 fee, uint256 allowance, bool stable) external ``` -_converts `originalAmount` with `originalDecimals` into -amount with `decidedDecimals`_ +adds a token to the stablecoins list. +can be called only from permissioned actor. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| originalAmount | uint256 | amount to convert | -| originalDecimals | uint256 | decimals of the original amount | -| decidedDecimals | uint256 | decimals for the output amount | - -#### Return Values - -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | uint256 | amount converted amount with `decidedDecimals` | +| token | address | token address | +| dataFeed | address | dataFeed address | +| fee | uint256 | 1% = 100 | +| allowance | uint256 | token allowance (decimals 18) | +| stable | bool | is stablecoin flag | -### convertFromBase18 +### removePaymentToken ```solidity -function convertFromBase18(uint256 originalAmount, uint256 decidedDecimals) internal pure returns (uint256) +function removePaymentToken(address token) external ``` -_converts `originalAmount` with decimals 18 into -amount with `decidedDecimals`_ +removes a token from stablecoins list. +can be called only from permissioned actor. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| originalAmount | uint256 | amount to convert | -| decidedDecimals | uint256 | decimals for the output amount | +| token | address | token address | -#### Return Values +### changeTokenAllowance + +```solidity +function changeTokenAllowance(address token, uint256 allowance) external +``` + +set new token allowance. +if type(uint256).max = infinite allowance +prev allowance rewrites by new +can be called only from permissioned actor. + +#### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | uint256 | amount converted amount with `decidedDecimals` | +| token | address | token address | +| allowance | uint256 | new allowance (decimals 18) | -### convertToBase18 +### changeTokenFee ```solidity -function convertToBase18(uint256 originalAmount, uint256 originalDecimals) internal pure returns (uint256) +function changeTokenFee(address token, uint256 fee) external ``` -_converts `originalAmount` with `originalDecimals` into -amount with decimals 18_ +set new token fee. +can be called only from permissioned actor. #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| originalAmount | uint256 | amount to convert | -| originalDecimals | uint256 | decimals of the original amount | +| token | address | token address | +| fee | uint256 | new fee percent 1% = 100 | -#### Return Values +### setVariationTolerance -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | uint256 | amount converted amount with 18 decimals | +```solidity +function setVariationTolerance(uint256 tolerance) external +``` -## MBtcMidasAccessControlRoles +set new prices diviation percent. +can be called only from permissioned actor. -Base contract that stores all roles descriptors for mBTC contracts +#### Parameters -### M_BTC_DEPOSIT_VAULT_ADMIN_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | +| tolerance | uint256 | new prices diviation percent 1% = 100 | + +### setMinAmount ```solidity -bytes32 M_BTC_DEPOSIT_VAULT_ADMIN_ROLE +function setMinAmount(uint256 newAmount) external ``` -actor that can manage MBtcDepositVault - -### M_BTC_REDEMPTION_VAULT_ADMIN_ROLE +set new min amount. +can be called only from permissioned actor. -```solidity -bytes32 M_BTC_REDEMPTION_VAULT_ADMIN_ROLE -``` +#### Parameters -actor that can manage MBtcRedemptionVault +| Name | Type | Description | +| ---- | ---- | ----------- | +| newAmount | uint256 | min amount for operations in mToken | -### M_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +### addWaivedFeeAccount ```solidity -bytes32 M_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +function addWaivedFeeAccount(address account) external ``` -actor that can manage MBtcCustomAggregatorFeed and MBtcDataFeed +adds a account to waived fee restriction. +can be called only from permissioned actor. -## MBtcRedemptionVault +#### Parameters -Smart contract that handles mBTC redemption +| Name | Type | Description | +| ---- | ---- | ----------- | +| account | address | user address | -### vaultRole +### removeWaivedFeeAccount ```solidity -function vaultRole() public pure returns (bytes32) +function removeWaivedFeeAccount(address account) external ``` -## TACmBtcMidasAccessControlRoles +removes a account from waived fee restriction. +can be called only from permissioned actor. -Base contract that stores all roles descriptors for TACmBTC contracts +#### Parameters -### TAC_M_BTC_DEPOSIT_VAULT_ADMIN_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | +| account | address | user address | + +### setFeeReceiver ```solidity -bytes32 TAC_M_BTC_DEPOSIT_VAULT_ADMIN_ROLE +function setFeeReceiver(address receiver) external ``` -actor that can manage TACmBtcDepositVault +set new receiver for fees. +can be called only from permissioned actor. -### TAC_M_BTC_REDEMPTION_VAULT_ADMIN_ROLE +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| receiver | address | new fee receiver address | + +### setTokensReceiver ```solidity -bytes32 TAC_M_BTC_REDEMPTION_VAULT_ADMIN_ROLE +function setTokensReceiver(address receiver) external ``` -actor that can manage TACmBtcRedemptionVault +set new receiver for tokens. +can be called only from permissioned actor. -## TACmBtcRedemptionVault +#### Parameters -Smart contract that handles TACmBTC redemption +| Name | Type | Description | +| ---- | ---- | ----------- | +| receiver | address | new token receiver address | -### vaultRole +### setInstantFee ```solidity -function vaultRole() public pure returns (bytes32) +function setInstantFee(uint256 newInstantFee) external ``` -## MBasisMidasAccessControlRoles +set operation fee percent. +can be called only from permissioned actor. -Base contract that stores all roles descriptors for mBASIS contracts +#### Parameters -### M_BASIS_DEPOSIT_VAULT_ADMIN_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | +| newInstantFee | uint256 | new instant operations fee percent 1& = 100 | + +### setMinMaxInstantFee ```solidity -bytes32 M_BASIS_DEPOSIT_VAULT_ADMIN_ROLE +function setMinMaxInstantFee(uint64 newMinInstantFee, uint64 newMaxInstantFee) external ``` -actor that can manage MBasisDepositVault - -### M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE +set new minimum/maximum instant fee -```solidity -bytes32 M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE -``` +#### Parameters -actor that can manage MBasisRedemptionVault +| Name | Type | Description | +| ---- | ---- | ----------- | +| newMinInstantFee | uint64 | new minimum instant fee | +| newMaxInstantFee | uint64 | new maximum instant fee | -### M_BASIS_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +### setInstantLimitConfig ```solidity -bytes32 M_BASIS_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +function setInstantLimitConfig(uint256 window, uint256 limit) external ``` -actor that can manage MBasisCustomAggregatorFeed and MBasisDataFeed +set operation limit configs. +can be called only from permissioned actor. -## MBasisRedemptionVault +#### Parameters -Smart contract that handles mBASIS minting +| Name | Type | Description | +| ---- | ---- | ----------- | +| window | uint256 | window duration in seconds | +| limit | uint256 | limit amount per window | -### vaultRole +### setMaxInstantShare ```solidity -function vaultRole() public pure returns (bytes32) +function setMaxInstantShare(uint64 newMaxInstantShare) external ``` -## MBasisRedemptionVaultWithBUIDL +set maximum instant share value in basis points (100 = 1%) -Smart contract that handles mBASIS minting +#### Parameters -### vaultRole +| Name | Type | Description | +| ---- | ---- | ----------- | +| newMaxInstantShare | uint64 | new maximum instant share value in basis points (100 = 1%) | + +### setMaxApproveRequestId ```solidity -function vaultRole() public pure returns (bytes32) +function setMaxApproveRequestId(uint256 newMaxApproveRequestId) external ``` -## MBasisRedemptionVaultWithSwapper +sets max requestId that can be approved -Smart contract that handles mBASIS redemptions +#### Parameters -### vaultRole +| Name | Type | Description | +| ---- | ---- | ----------- | +| newMaxApproveRequestId | uint256 | new max requestId that can be approved | + +### removeInstantLimitConfig ```solidity -function vaultRole() public pure returns (bytes32) +function removeInstantLimitConfig(uint256 window) external ``` -## MEdgeMidasAccessControlRoles +remove operation limit config. +can be called only from permissioned actor. -Base contract that stores all roles descriptors for mEDGE contracts +#### Parameters -### M_EDGE_DEPOSIT_VAULT_ADMIN_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | +| window | uint256 | window duration in seconds | + +### freeFromMinAmount ```solidity -bytes32 M_EDGE_DEPOSIT_VAULT_ADMIN_ROLE +function freeFromMinAmount(address user, bool enable) external ``` -actor that can manage MEdgeDepositVault +frees given `user` from the minimal deposit +amount validation in `initiateDepositRequest` -### M_EDGE_REDEMPTION_VAULT_ADMIN_ROLE +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| user | address | address of user | +| enable | bool | | + +### withdrawToken ```solidity -bytes32 M_EDGE_REDEMPTION_VAULT_ADMIN_ROLE +function withdrawToken(address token, uint256 amount) external ``` -actor that can manage MEdgeRedemptionVault +withdraws `amount` of a given `token` from the contract +to the `tokensReceiver` address -### M_EDGE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +#### Parameters -```solidity -bytes32 M_EDGE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE -``` +| Name | Type | Description | +| ---- | ---- | ----------- | +| token | address | token address | +| amount | uint256 | token amount | -actor that can manage MEdgeCustomAggregatorFeed and MEdgeDataFeed +## IMidasAccessControl -## MEdgeRedemptionVaultWithSwapper +### SetUserFacingRoleParams -Smart contract that handles mEDGE redemptions +#### Parameters -### vaultRole +| Name | Type | Description | +| ---- | ---- | ----------- | ```solidity -function vaultRole() public pure returns (bytes32) +struct SetUserFacingRoleParams { + bytes32 masterRole; + bool enabled; +} ``` -## TACmEdgeMidasAccessControlRoles +### SetGrantOperatorRoleParams -Base contract that stores all roles descriptors for TACmEdge contracts +#### Parameters -### TAC_M_EDGE_DEPOSIT_VAULT_ADMIN_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | ```solidity -bytes32 TAC_M_EDGE_DEPOSIT_VAULT_ADMIN_ROLE +struct SetGrantOperatorRoleParams { + bytes32 masterRole; + address targetContract; + bytes4 functionSelector; + address operator; + bool enabled; +} ``` -actor that can manage TACmEdgeDepositVault +### SetPermissionRoleParams -### TAC_M_EDGE_REDEMPTION_VAULT_ADMIN_ROLE +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | ```solidity -bytes32 TAC_M_EDGE_REDEMPTION_VAULT_ADMIN_ROLE +struct SetPermissionRoleParams { + bytes32 masterRole; + address targetContract; + bytes4 functionSelector; + address account; + bool enabled; +} ``` -actor that can manage TACmEdgeRedemptionVault - -## TACmEdgeRedemptionVault - -Smart contract that handles TACmEDGE redemption - -### vaultRole +### SetUserFacingRole ```solidity -function vaultRole() public pure returns (bytes32) +event SetUserFacingRole(bytes32 masterRole, bool enabled) ``` -## MFOneMidasAccessControlRoles +#### Parameters -Base contract that stores all roles descriptors for mF-ONE contracts +| Name | Type | Description | +| ---- | ---- | ----------- | +| masterRole | bytes32 | OZ role for the scope | +| enabled | bool | whether that role may manage grant operators for the scope. | -### M_FONE_DEPOSIT_VAULT_ADMIN_ROLE +### SetGrantOperatorRole ```solidity -bytes32 M_FONE_DEPOSIT_VAULT_ADMIN_ROLE +event SetGrantOperatorRole(bytes32 masterRole, address targetContract, bytes4 functionSelector, address operator, bool enabled) ``` -actor that can manage MFOneDepositVault +#### Parameters -### M_FONE_REDEMPTION_VAULT_ADMIN_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | +| masterRole | bytes32 | OZ role for the scope | +| targetContract | address | contract whose function is scoped. | +| functionSelector | bytes4 | selector of the scoped function. | +| operator | address | address that may call `setFunctionPermission` for this scope. | +| enabled | bool | grant or revoke grant-operator status. | + +### SetPermissionRole ```solidity -bytes32 M_FONE_REDEMPTION_VAULT_ADMIN_ROLE +event SetPermissionRole(bytes32 masterRole, address targetContract, address account, bytes4 functionSelector, bool enabled) ``` -actor that can manage MFOneRedemptionVault +#### Parameters -### M_FONE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | +| masterRole | bytes32 | OZ role for the scope | +| targetContract | address | contract whose function is scoped. | +| account | address | address receiving or losing permission | +| functionSelector | bytes4 | selector of the scoped function. | +| enabled | bool | grant or revoke | + +### setUserFacingRoleMult ```solidity -bytes32 M_FONE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +function setUserFacingRoleMult(struct IMidasAccessControl.SetUserFacingRoleParams[] params) external ``` -actor that can manage MFOneCustomAggregatorFeed and MFOneDataFeed +Enable or disable which OZ role may administer function-access scopes for that role. -## MFOneRedemptionVaultWithSwapper +_Only `DEFAULT_ADMIN_ROLE` can call this function. +Prevents unrelated role admins from spamming access mappings._ -Smart contract that handles mF-ONE redemptions +#### Parameters -### vaultRole +| Name | Type | Description | +| ---- | ---- | ----------- | +| params | struct IMidasAccessControl.SetUserFacingRoleParams[] | array of SetUserFacingRoleParams | + +### setGrantOperatorRoleMult ```solidity -function vaultRole() public pure returns (bytes32) +function setGrantOperatorRoleMult(struct IMidasAccessControl.SetGrantOperatorRoleParams[] params) external ``` -## MLiquidityMidasAccessControlRoles +Add or remove a grant operator for a specific contract function scope. -Base contract that stores all roles descriptors for mLIQUIDITY contracts +_Caller must hold `masterRole`; role must be enabled via `setFunctionAccessAdminRoleEnabled`._ -### M_LIQUIDITY_DEPOSIT_VAULT_ADMIN_ROLE +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| params | struct IMidasAccessControl.SetGrantOperatorRoleParams[] | array of SetGrantOperatorRoleParams | + +### setPermissionRoleMult ```solidity -bytes32 M_LIQUIDITY_DEPOSIT_VAULT_ADMIN_ROLE +function setPermissionRoleMult(struct IMidasAccessControl.SetPermissionRoleParams[] params) external ``` -actor that can manage MLiquidityDepositVault +Grant or revoke function access for an account -### M_LIQUIDITY_REDEMPTION_VAULT_ADMIN_ROLE +_caller must be a grant operator for the scope_ -```solidity -bytes32 M_LIQUIDITY_REDEMPTION_VAULT_ADMIN_ROLE -``` +#### Parameters -actor that can manage MLiquidityRedemptionVault +| Name | Type | Description | +| ---- | ---- | ----------- | +| params | struct IMidasAccessControl.SetPermissionRoleParams[] | array of SetPermissionRoleParams | -### M_LIQUIDITY_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +### setRoleAdmin ```solidity -bytes32 M_LIQUIDITY_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +function setRoleAdmin(bytes32 role, bytes32 newAdminRole) external ``` -actor that can manage MLiquidityCustomAggregatorFeed and MLiquidityDataFeed +set the admin role for a specific role -## MLiquidityRedemptionVault +_can be called only by the address that holds `DEFAULT_ADMIN_ROLE`_ -Smart contract that handles mLIQUIDITY redemptions +#### Parameters -### vaultRole +| Name | Type | Description | +| ---- | ---- | ----------- | +| role | bytes32 | the role to set the admin role for | +| newAdminRole | bytes32 | the new admin role | + +### isFunctionAccessGrantOperator ```solidity -function vaultRole() public pure returns (bytes32) +function isFunctionAccessGrantOperator(bytes32 masterRole, address targetContract, bytes4 functionSelector, address operator) external view returns (bool) ``` -## MMevMidasAccessControlRoles +Whether `operator` may call `setFunctionPermission` for the function scope -Base contract that stores all roles descriptors for mMEV contracts +#### Parameters -### M_MEV_DEPOSIT_VAULT_ADMIN_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | +| masterRole | bytes32 | OZ role for the scope | +| targetContract | address | scoped contract | +| functionSelector | bytes4 | scoped function | +| operator | address | address checked for grant-operator status | -```solidity -bytes32 M_MEV_DEPOSIT_VAULT_ADMIN_ROLE -``` +#### Return Values -actor that can manage MMevDepositVault +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | allowed whether `operator` is a grant operator for the scope | -### M_MEV_REDEMPTION_VAULT_ADMIN_ROLE +### hasFunctionPermission ```solidity -bytes32 M_MEV_REDEMPTION_VAULT_ADMIN_ROLE +function hasFunctionPermission(bytes32 masterRole, address targetContract, bytes4 functionSelector, address account) external view returns (bool) ``` -actor that can manage MMevRedemptionVault - -### M_MEV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +Whether `account` may call the scoped function on `targetContract`. -```solidity -bytes32 M_MEV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE -``` +#### Parameters -actor that can manage MMevCustomAggregatorFeed and MMevDataFeed +| Name | Type | Description | +| ---- | ---- | ----------- | +| masterRole | bytes32 | OZ role for the scope | +| targetContract | address | scoped contract | +| functionSelector | bytes4 | scoped function | +| account | address | address checked for permissio. | -## MMevRedemptionVaultWithSwapper +#### Return Values -Smart contract that handles mMEV redemptions +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | allowed whether `account` has function access for the scope | -### vaultRole +## Request -```solidity -function vaultRole() public pure returns (bytes32) -``` +Legacy Redeem request scruct -## TACmMevMidasAccessControlRoles +_used for backward compatibility_ -Base contract that stores all roles descriptors for TACmMEV contracts +### Parameters -### TAC_M_MEV_DEPOSIT_VAULT_ADMIN_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | ```solidity -bytes32 TAC_M_MEV_DEPOSIT_VAULT_ADMIN_ROLE +struct Request { + address sender; + address tokenOut; + enum RequestStatus status; + uint256 amountMToken; + uint256 mTokenRate; + uint256 tokenOutRate; +} ``` -actor that can manage TACmMevDepositVault - -### TAC_M_MEV_REDEMPTION_VAULT_ADMIN_ROLE - -```solidity -bytes32 TAC_M_MEV_REDEMPTION_VAULT_ADMIN_ROLE -``` +## RequestV2 -actor that can manage TACmMevRedemptionVault +Redeem request v2 scruct -## TACmMevRedemptionVault +_replaces `Request` struct and adds `feePercent`, `amountMTokenInstant`, `approvedMTokenRate` and `version` fields_ -Smart contract that handles TACmMEV redemption +### Parameters -### vaultRole +| Name | Type | Description | +| ---- | ---- | ----------- | ```solidity -function vaultRole() public pure returns (bytes32) +struct RequestV2 { + address sender; + address tokenOut; + enum RequestStatus status; + uint256 amountMToken; + uint256 mTokenRate; + uint256 tokenOutRate; + uint256 feePercent; + uint256 amountMTokenInstant; + uint256 approvedMTokenRate; + uint8 version; +} ``` -## MRe7MidasAccessControlRoles - -Base contract that stores all roles descriptors for mRE7 contracts - -### M_RE7_DEPOSIT_VAULT_ADMIN_ROLE +## RedemptionVaultInitParams ```solidity -bytes32 M_RE7_DEPOSIT_VAULT_ADMIN_ROLE +struct RedemptionVaultInitParams { + address requestRedeemer; +} ``` -actor that can manage MRe7DepositVault - -### M_RE7_REDEMPTION_VAULT_ADMIN_ROLE +## RedemptionVaultV2InitParams ```solidity -bytes32 M_RE7_REDEMPTION_VAULT_ADMIN_ROLE +struct RedemptionVaultV2InitParams { + address loanLp; + address loanLpFeeReceiver; + address loanRepaymentAddress; + address loanSwapperVault; + uint64 maxLoanApr; +} ``` -actor that can manage MRe7RedemptionVault +## LiquidityProviderLoanRequest + +```solidity +struct LiquidityProviderLoanRequest { + address tokenOut; + uint256 amountTokenOut; + uint256 amountFee; + uint256 createdAt; + enum RequestStatus status; +} +``` + +## IRedemptionVault + +### RedeemInstant + +```solidity +event RedeemInstant(address user, address tokenOut, address recipient, uint256 amount, uint256 feeAmount, uint256 amountTokenOut) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| user | address | function caller (msg.sender) | +| tokenOut | address | address of tokenOut | +| recipient | address | | +| amount | uint256 | amount of mToken | +| feeAmount | uint256 | fee amount in tokenOut | +| amountTokenOut | uint256 | amount of tokenOut | + +### RedeemRequest + +```solidity +event RedeemRequest(uint256 requestId, address user, address tokenOut, address recipient, uint256 amountMTokenIn, uint256 amountMTokenInstant, uint256 mTokenRate, uint256 feePercent) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | +| user | address | function caller (msg.sender) | +| tokenOut | address | address of tokenOut | +| recipient | address | recipient address | +| amountMTokenIn | uint256 | amount of mToken | +| amountMTokenInstant | uint256 | amount of mToken that was redeemed instantly | +| mTokenRate | uint256 | mToken rate | +| feePercent | uint256 | fee percent | + +### CreateLiquidityProviderLoanRequest + +```solidity +event CreateLiquidityProviderLoanRequest(uint256 loanId, address tokenOut, uint256 amountTokenOut, uint256 amountFee, uint256 mTokenRate, uint256 tokenOutRate) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| loanId | uint256 | loan id | +| tokenOut | address | tokenOut address | +| amountTokenOut | uint256 | amount of tokenOut | +| amountFee | uint256 | fee amount in payment token | +| mTokenRate | uint256 | mToken rate | +| tokenOutRate | uint256 | tokenOut rate | + +### ApproveRequest + +```solidity +event ApproveRequest(uint256 requestId, uint256 newMTokenRate, bool isSafe, bool isAvgRate) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | mint request id | +| newMTokenRate | uint256 | new mToken rate | +| isSafe | bool | if true, approval is safe | +| isAvgRate | bool | if true, newMtokenRate is avg rate | + +### RejectRequest + +```solidity +event RejectRequest(uint256 requestId, address user) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | mint request id | +| user | address | address of user | + +### SetRequestRedeemer + +```solidity +event SetRequestRedeemer(address caller, address redeemer) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | function caller (msg.sender) | +| redeemer | address | new address of request redeemer | + +### SetLoanLpFeeReceiver + +```solidity +event SetLoanLpFeeReceiver(address caller, address newLoanLpFeeReceiver) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | function caller (msg.sender) | +| newLoanLpFeeReceiver | address | new address of loan liquidity provider fee receiver | + +### SetLoanLp + +```solidity +event SetLoanLp(address caller, address newLoanLp) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | function caller (msg.sender) | +| newLoanLp | address | new address of loan liquidity provider | + +### SetLoanRepaymentAddress + +```solidity +event SetLoanRepaymentAddress(address caller, address newLoanRepaymentAddress) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | function caller (msg.sender) | +| newLoanRepaymentAddress | address | new address of loan repayment address | + +### SetLoanSwapperVault + +```solidity +event SetLoanSwapperVault(address caller, address newLoanSwapperVault) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | function caller (msg.sender) | +| newLoanSwapperVault | address | new address of loan swapper vault | + +### SetMaxLoanApr + +```solidity +event SetMaxLoanApr(address caller, uint64 newMaxLoanApr) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | function caller (msg.sender) | +| newMaxLoanApr | uint64 | new maximum loan APR value in basis points (100 = 1%) | + +### SetPreferLoanLiquidity + +```solidity +event SetPreferLoanLiquidity(address caller, bool newLoanLpFirst) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | function caller (msg.sender) | +| newLoanLpFirst | bool | new flag to determine if the loan LP liquidity should be used first | + +### RepayLpLoanRequest + +```solidity +event RepayLpLoanRequest(address caller, uint256 requestId) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | function caller (msg.sender) | +| requestId | uint256 | request id | + +### CancelLpLoanRequest + +```solidity +event CancelLpLoanRequest(address caller, uint256 requestId) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | function caller (msg.sender) | +| requestId | uint256 | request id | + +### redeemInstant + +```solidity +function redeemInstant(address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount) external +``` + +redeem mToken to tokenOut if daily limit and allowance not exceeded +Burns mToken from the user. +Transfers fee in mToken to feeReceiver +Transfers tokenOut to user. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | stable coin token address to redeem to | +| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | +| minReceiveAmount | uint256 | minimum expected amount of tokenOut to receive (decimals 18) | + +### redeemInstant + +```solidity +function redeemInstant(address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount, address recipient) external +``` + +Does the same as original `redeemInstant` but allows specifying a custom tokensReceiver address. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | stable coin token address to redeem to | +| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | +| minReceiveAmount | uint256 | minimum expected amount of tokenOut to receive (decimals 18) | +| recipient | address | address that receives tokens | + +### redeemRequest + +```solidity +function redeemRequest(address tokenOut, uint256 amountMTokenIn) external returns (uint256) +``` + +creating redeem request +Transfers amount in mToken to contract +Transfers fee in mToken to feeReceiver + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | stable coin token address to redeem to | +| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | request id | + +### redeemRequest + +```solidity +function redeemRequest(address tokenOut, uint256 amountMTokenIn, address recipient) external returns (uint256) +``` + +Does the same as original `redeemRequest` but allows specifying a custom tokensReceiver address. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | stable coin token address to redeem to | +| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | +| recipient | address | address that receives tokens | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | request id | + +### redeemRequest + +```solidity +function redeemRequest(address tokenOut, uint256 amountMTokenIn, address recipientRequest, uint256 instantShare, uint256 minReceiveAmountInstantShare, address recipientInstant) external returns (uint256) +``` + +Instantly redeems `instantShare` amount of `amountMTokenIn` and creates a request for the remaining amount. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | stable coin token address to redeem to | +| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | +| recipientRequest | address | address that receives tokens for the request part | +| instantShare | uint256 | % amount of `amountMTokenIn` that will be redeemed instantly | +| minReceiveAmountInstantShare | uint256 | min receive amount for the instant share | +| recipientInstant | address | address that receives tokens for the instant part | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | request id | + +### safeBulkApproveRequestAtSavedRate + +```solidity +function safeBulkApproveRequestAtSavedRate(uint256[] requestIds) external +``` + +approving requests from the `requestIds` array with the mToken rate +from the request. WONT fail even if there is not enough liquidity +to process all requests. +Does same validation as `safeApproveRequest`. +Transfers tokenOut to users +Sets request flags to Processed. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestIds | uint256[] | request ids array | + +### safeBulkApproveRequest + +```solidity +function safeBulkApproveRequest(uint256[] requestIds) external +``` + +approving requests from the `requestIds` array with the +current mToken rate. WONT fail even if there is not enough liquidity +to process all requests. +Does same validation as `safeApproveRequest`. +Transfers tokenOut to users +Sets request flags to Processed. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestIds | uint256[] | request ids array | + +### safeBulkApproveRequestAvgRate + +```solidity +function safeBulkApproveRequestAvgRate(uint256[] requestIds) external +``` + +approving requests from the `requestIds` array with the +current mToken rate as avg rate. WONT fail even if there is not enough liquidity +to process all requests. +Does same validation as `safeApproveRequestAvgRate`. +Transfers tokenOut to users +Sets request flags to Processed. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestIds | uint256[] | request ids array | + +### safeBulkApproveRequest + +```solidity +function safeBulkApproveRequest(uint256[] requestIds, uint256 newMTokenRate) external +``` + +approving requests from the `requestIds` array using the `newMTokenRate`. +WONT fail even if there is not enough liquidity to process all requests. +Does same validation as `safeApproveRequest`. +Transfers tokenOut to user +Sets request flags to Processed. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestIds | uint256[] | request ids array | +| newMTokenRate | uint256 | new mToken rate inputted by vault admin | + +### safeBulkApproveRequestAvgRate + +```solidity +function safeBulkApproveRequestAvgRate(uint256[] requestIds, uint256 avgMTokenRate) external +``` + +approving requests from the `requestIds` array using the `avgMTokenRate`. +WONT fail even if there is not enough liquidity to process all requests. +Does same validation as `safeApproveRequestAvgRate`. +Transfers tokenOut to user +Sets request flags to Processed. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestIds | uint256[] | request ids array | +| avgMTokenRate | uint256 | avg mToken rate inputted by vault admin | + +### approveRequest + +```solidity +function approveRequest(uint256 requestId, uint256 newMTokenRate) external +``` + +approving redeem request if not exceed tokenOut allowance +Burns amount mToken from contract +Transfers tokenOut to user +Sets flag Processed + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | +| newMTokenRate | uint256 | new mToken rate inputted by vault admin | + +### approveRequestAvgRate + +```solidity +function approveRequestAvgRate(uint256 requestId, uint256 avgMTokenRate) external +``` + +approving redeem request if not exceed tokenOut allowance +Burns amount mToken from contract +Transfers tokenOut to user +Sets flag Processed + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | +| avgMTokenRate | uint256 | avg mToken rate inputted by vault admin | + +### safeApproveRequest + +```solidity +function safeApproveRequest(uint256 requestId, uint256 newMTokenRate) external +``` + +approving request if inputted token rate fit price diviation percent +Burns amount mToken from contract +Transfers tokenOut to user +Sets flag Processed + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | +| newMTokenRate | uint256 | new mToken rate inputted by vault admin | + +### safeApproveRequestAvgRate + +```solidity +function safeApproveRequestAvgRate(uint256 requestId, uint256 avgMTokenRate) external +``` + +approving request if inputted token rate fit price diviation percent +Burns amount mToken from contract +Transfers tokenOut to user +Sets flag Processed + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | +| avgMTokenRate | uint256 | avg mToken rate inputted by vault admin | + +### rejectRequest + +```solidity +function rejectRequest(uint256 requestId) external +``` + +rejecting request +Sets request flag to Canceled. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | + +### bulkRepayLpLoanRequest + +```solidity +function bulkRepayLpLoanRequest(uint256[] requestIds, uint64 loanApr) external +``` + +repaying loan requests from the `requestIds` array +Transfers tokenOut to loan repayment address +Transfers fee in tokenOut to loan lp fee receiver +Sets request flags to Processed. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestIds | uint256[] | request ids array | +| loanApr | uint64 | loan APR. Overrides calculated loan fee in case if accrued interest is greater than the calculated loan fee. | + +### cancelLpLoanRequest + +```solidity +function cancelLpLoanRequest(uint256 requestId) external +``` + +canceling loan request +Sets request flags to Canceled. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | + +### setRequestRedeemer + +```solidity +function setRequestRedeemer(address redeemer) external +``` + +set address which is designated for standard redemptions, allowing tokens to be pulled from this address + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| redeemer | address | new address of request redeemer | + +### setLoanLpFeeReceiver + +```solidity +function setLoanLpFeeReceiver(address newLoanLpFeeReceiver) external +``` + +set address of loan liquidity provider fee receiver + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newLoanLpFeeReceiver | address | new address of loan liquidity provider fee receiver | + +### setLoanLp + +```solidity +function setLoanLp(address newLoanLp) external +``` + +set address of loan liquidity provider + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newLoanLp | address | new address of loan liquidity provider | + +### setLoanRepaymentAddress + +```solidity +function setLoanRepaymentAddress(address newLoanRepaymentAddress) external +``` + +set address of loan repayment address + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newLoanRepaymentAddress | address | new address of loan repayment address | + +### setLoanSwapperVault + +```solidity +function setLoanSwapperVault(address newLoanSwapperVault) external +``` + +set address of loan swapper vault + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newLoanSwapperVault | address | new address of loan swapper vault | + +### setMaxLoanApr + +```solidity +function setMaxLoanApr(uint64 newMaxLoanApr) external +``` + +set maximum loan APR value in basis points (100 = 1%) + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newMaxLoanApr | uint64 | new maximum loan APR value in basis points (100 = 1%) | + +### setPreferLoanLiquidity + +```solidity +function setPreferLoanLiquidity(bool newLoanLpFirst) external +``` + +set flag to determine if the loan LP liquidity should be used first + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newLoanLpFirst | bool | new flag to determine if the loan LP liquidity should be used first | + +## ISanctionsList + +### isSanctioned + +```solidity +function isSanctioned(address addr) external view returns (bool) +``` + +## IAaveV3Pool + +Minimal interface for the Aave V3 Pool (v3.2+) + +_Full interface: https://github.com/aave-dao/aave-v3-origin/blob/main/src/contracts/interfaces/IPool.sol_ + +### withdraw + +```solidity +function withdraw(address asset, uint256 amount, address to) external returns (uint256) +``` + +Withdraws an `amount` of underlying asset from the reserve, burning the equivalent aTokens owned +E.g. User has 100 aUSDC, calls withdraw() and receives 100 USDC, burning the 100 aUSDC + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| asset | address | The address of the underlying asset to withdraw | +| amount | uint256 | The underlying amount to be withdrawn - Send the value type(uint256).max in order to withdraw the whole aToken balance | +| to | address | The address that will receive the underlying, same as msg.sender if the user wants to receive it on his own wallet, or a different address if the beneficiary is a different wallet | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | The final amount withdrawn | + +### supply + +```solidity +function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external +``` + +Supplies an `amount` of underlying asset into the reserve, receiving in return overlying aTokens. +- E.g. User supplies 100 USDC and gets in return 100 aUSDC + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| asset | address | The address of the underlying asset to supply | +| amount | uint256 | The amount to be supplied | +| onBehalfOf | address | The address that will receive the aTokens, same as msg.sender if the user wants to receive them on his own wallet, or a different address if the beneficiary of aTokens is a different wallet | +| referralCode | uint16 | Code used to register the integrator originating the operation, for potential rewards. 0 if the action is executed directly by the user, without any middle-man | + +### getReserveAToken + +```solidity +function getReserveAToken(address asset) external view returns (address) +``` + +Returns the aToken address of a reserve + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| asset | address | The address of the underlying asset of the reserve | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | address | The aToken address of the reserve | + +## IAcreAdapter + +Interface for the Vault contract. + +_This interface is used to interact with the Vault contract. + It is used to deposit and redeem shares. + It is used to get the price of the shares with convertToShares and convertToAssets. + It is used to request an asynchronous redemption of shares. + It assumes no fees are charged on deposits or redemptions._ + +### Deposit + +```solidity +event Deposit(address sender, address owner, uint256 assets, uint256 shares) +``` + +Emitted when assets are deposited into the vault. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| sender | address | The address that deposited the assets. | +| owner | address | The address that received the shares. | +| assets | uint256 | The amount of assets deposited. | +| shares | uint256 | The amount of shares received. | + +### RedeemRequest + +```solidity +event RedeemRequest(uint256 requestId, address sender, address receiver, uint256 shares) +``` + +Emitted when a redeem request is made. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | The request ID. | +| sender | address | The address that made the request. | +| receiver | address | The address that will received the assets. | +| shares | uint256 | The amount of shares that would be redeemed. | + +### share + +```solidity +function share() external view returns (address shareTokenAddress) +``` + +_Returns the address of the share token. The address MAY be the same + as the vault address. + +- MUST be an ERC-20 token contract. +- MUST NOT revert._ + +### asset + +```solidity +function asset() external view returns (address assetTokenAddress) +``` + +_Returns the address of the asset token. + +- MUST be an ERC-20 token contract. +- MUST NOT revert._ + +### convertToShares + +```solidity +function convertToShares(uint256 assets) external view returns (uint256 shares) +``` + +_Returns the amount of shares that the Vault would exchange for the amount of assets provided, in an ideal +scenario where all the conditions are met. + +- MUST NOT be inclusive of any fees that are charged against assets in the Vault. +- MUST NOT show any variations depending on the caller. +- MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. +- MUST NOT revert. + +NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the +“average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and +from._ + +### convertToAssets + +```solidity +function convertToAssets(uint256 shares) external view returns (uint256 assets) +``` + +_Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an ideal +scenario where all the conditions are met. + +- MUST NOT be inclusive of any fees that are charged against assets in the Vault. +- MUST NOT show any variations depending on the caller. +- MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. +- MUST NOT revert. + +NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the +“average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and +from._ + +### deposit + +```solidity +function deposit(uint256 assets, address receiver) external returns (uint256 shares) +``` + +_Mints shares Vault shares to owner by depositing exactly amount of underlying tokens. + +- MUST emit the Deposit event._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| assets | uint256 | The amount of assets to be deposited. | +| receiver | address | The address that will received the shares. NOTE: Implementation requires pre-approval of the Vault with the Vault’s underlying asset token. | + +### requestRedeem + +```solidity +function requestRedeem(uint256 shares, address receiver) external returns (uint256 requestId) +``` + +_Assumes control of shares from sender into the Vault and submits a Request for asynchronous redeem. + +- MUST emit the RedeemRequest event. +- Once a request is finalized MUST emit the RedeemFinalize event._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| shares | uint256 | The amount of shares to be redeemed. | +| receiver | address | The address that will receive assets on request finalization. NOTE: Implementations requires pre-approval of the Vault with the Vault's share token. | + +## IMorphoVault + +Morpho Vault interface extending the ERC-4626 Tokenized Vault Standard + +_Works with both Morpho Vaults V1 (MetaMorpho) and V2 +V1 repo: https://github.com/morpho-org/metamorpho-v1.1 +V2 repo: https://github.com/morpho-org/vault-v2_ + +## ISuperstateToken + +### StablecoinConfig + +```solidity +struct StablecoinConfig { + address sweepDestination; + uint96 fee; +} +``` + +### subscribe + +```solidity +function subscribe(address to, uint256 inAmount, address stablecoin) external +``` + +### setStablecoinConfig + +```solidity +function setStablecoinConfig(address stablecoin, address newSweepDestination, uint96 newFee) external +``` + +### supportedStablecoins + +```solidity +function supportedStablecoins(address stablecoin) external view returns (struct ISuperstateToken.StablecoinConfig) +``` + +### symbol + +```solidity +function symbol() external view returns (string) +``` + +### owner + +```solidity +function owner() external view returns (address) +``` + +### allowListV2 + +```solidity +function allowListV2() external view returns (address) +``` + +### isAllowed + +```solidity +function isAllowed(address addr) external view returns (bool) +``` + +## DecimalsCorrectionLibrary + +### convert + +```solidity +function convert(uint256 originalAmount, uint256 originalDecimals, uint256 decidedDecimals) internal pure returns (uint256) +``` + +_converts `originalAmount` with `originalDecimals` into +amount with `decidedDecimals`_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| originalAmount | uint256 | amount to convert | +| originalDecimals | uint256 | decimals of the original amount | +| decidedDecimals | uint256 | decimals for the output amount | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | amount converted amount with `decidedDecimals` | + +### convertFromBase18 + +```solidity +function convertFromBase18(uint256 originalAmount, uint256 decidedDecimals) internal pure returns (uint256) +``` + +_converts `originalAmount` with decimals 18 into +amount with `decidedDecimals`_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| originalAmount | uint256 | amount to convert | +| decidedDecimals | uint256 | decimals for the output amount | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | amount converted amount with `decidedDecimals` | + +### convertToBase18 + +```solidity +function convertToBase18(uint256 originalAmount, uint256 originalDecimals) internal pure returns (uint256) +``` + +_converts `originalAmount` with `originalDecimals` into +amount with decimals 18_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| originalAmount | uint256 | amount to convert | +| originalDecimals | uint256 | decimals of the original amount | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | amount converted amount with 18 decimals | + +## AcreAdapter + +Wrapper for Midas Vaults to be used by Acre protocol + +### depositVault + +```solidity +address depositVault +``` + +### redemptionVault + +```solidity +address redemptionVault +``` + +### mTokenDataFeed + +```solidity +address mTokenDataFeed +``` + +### assetTokenDecimals + +```solidity +uint256 assetTokenDecimals +``` + +### constructor + +```solidity +constructor(address depositVault_, address redemptionVault_, address assetToken_) public +``` + +constructor + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| depositVault_ | address | address of deposit vault contract (IDepositVault) | +| redemptionVault_ | address | address of redemption vault contract (IRedemptionVault) | +| assetToken_ | address | address of ERC20 asset token contract | + +### deposit + +```solidity +function deposit(uint256 assets, address receiver) external returns (uint256 shares) +``` + +_Mints shares Vault shares to owner by depositing exactly amount of underlying tokens. + +- MUST emit the Deposit event._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| assets | uint256 | The amount of assets to be deposited. | +| receiver | address | The address that will received the shares. NOTE: Implementation requires pre-approval of the Vault with the Vault’s underlying asset token. | + +### requestRedeem + +```solidity +function requestRedeem(uint256 shares, address receiver) external returns (uint256 requestId) +``` + +_Assumes control of shares from sender into the Vault and submits a Request for asynchronous redeem. + +- MUST emit the RedeemRequest event. +- Once a request is finalized MUST emit the RedeemFinalize event._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| shares | uint256 | The amount of shares to be redeemed. | +| receiver | address | The address that will receive assets on request finalization. NOTE: Implementations requires pre-approval of the Vault with the Vault's share token. | + +### convertToShares + +```solidity +function convertToShares(uint256 assets) external view returns (uint256) +``` + +_Returns the amount of shares that the Vault would exchange for the amount of assets provided, in an ideal +scenario where all the conditions are met. + +- MUST NOT be inclusive of any fees that are charged against assets in the Vault. +- MUST NOT show any variations depending on the caller. +- MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. +- MUST NOT revert. + +NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the +“average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and +from._ + +### convertToAssets + +```solidity +function convertToAssets(uint256 shares) external view returns (uint256) +``` + +_Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an ideal +scenario where all the conditions are met. + +- MUST NOT be inclusive of any fees that are charged against assets in the Vault. +- MUST NOT show any variations depending on the caller. +- MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. +- MUST NOT revert. + +NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the +“average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and +from._ + +### share + +```solidity +function share() public view returns (address) +``` + +_Returns the address of the share token. The address MAY be the same + as the vault address. + +- MUST be an ERC-20 token contract. +- MUST NOT revert._ + +### asset + +```solidity +function asset() public view returns (address) +``` + +_Returns the address of the asset token. + +- MUST be an ERC-20 token contract. +- MUST NOT revert._ + +## MidasAxelarVaultExecutable + +This contract is a InterchainTokenExecutable contract that allows deposits and redemptions operations against a + synchronous vault across different chains using Axelar's ITS protocol. + +_The contract is designed to handle deposits and redemptions of vault mTokens and paymentTokens, + ensuring that the mToken and paymentToken are correctly managed and transferred across chains. + It also includes slippage protection and refund mechanisms for failed transactions._ + +### TokenAddressMismatch + +```solidity +error TokenAddressMismatch(address itsTokenValue, address dvValue, address rvValue) +``` + +error for token address mismatch + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| itsTokenValue | address | address of ITS token | +| dvValue | address | address of mToken of deposit vault | +| rvValue | address | address of mToken of redemption vault | + +### depositVault + +```solidity +contract IDepositVault depositVault +``` + +getter for the deposit vault + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### redemptionVault + +```solidity +contract IRedemptionVault redemptionVault +``` + +getter for the redemption vault + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### paymentTokenId + +```solidity +bytes32 paymentTokenId +``` + +getter for the paymentToken ITS id + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### paymentTokenErc20 + +```solidity +address paymentTokenErc20 +``` + +getter for the paymentToken ERC20 + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### mTokenId + +```solidity +bytes32 mTokenId +``` + +getter for the mToken ITS id + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### mTokenErc20 + +```solidity +address mTokenErc20 +``` + +getter for the mToken ERC20 + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### paymentTokenDecimals + +```solidity +uint8 paymentTokenDecimals +``` + +decimals of `paymentTokenErc20` + +### chainNameHash + +```solidity +bytes32 chainNameHash +``` + +hash of the current chain name + +### constructor + +```solidity +constructor(address _depositVault, address _redemptionVault, bytes32 _paymentTokenId, bytes32 _mTokenId, address _interchainTokenService) public +``` + +### initialize + +```solidity +function initialize() external +``` + +Initializes the contract + +### _executeWithInterchainToken + +```solidity +function _executeWithInterchainToken(bytes32 commandId, string sourceChain, bytes sourceAddress, bytes data, bytes32 tokenId, address, uint256 amount) internal +``` + +internal function to execute the interchain token transfer + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| commandId | bytes32 | the commandId of the operation | +| sourceChain | string | the source chain of the operation | +| sourceAddress | bytes | the source address of the operation | +| data | bytes | the data of the operation | +| tokenId | bytes32 | the ITS tokenId of the operation | +| | address | | +| amount | uint256 | the amount of the operation | + +### handleExecuteWithInterchainToken + +```solidity +function handleExecuteWithInterchainToken(bytes _sourceAddress, bytes _data, bytes32 _tokenId, uint256 _amount) external +``` + +internal function to execute the interchain token transfer + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _sourceAddress | bytes | the source address of the operation | +| _data | bytes | the data of the operation | +| _tokenId | bytes32 | the ITS tokenId of the operation | +| _amount | uint256 | the amount of the operation | + +### depositAndSend + +```solidity +function depositAndSend(uint256 _paymentTokenAmount, bytes _data) external payable +``` + +deposits and sends the paymentToken to the destination chain + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _paymentTokenAmount | uint256 | the amount of payment tokens to deposit | +| _data | bytes | encoded data for the deposit. Expected data: abi.encode(bytes receiver,uint256 minReceiveAmount,bytes32 referrerId,string receiverChainName); | + +### redeemAndSend + +```solidity +function redeemAndSend(uint256 _mTokenAmount, bytes _data) external payable +``` + +redeems and sends the mToken to the destination chain + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _mTokenAmount | uint256 | the amount of m tokens to redeem | +| _data | bytes | encoded data for the redemption Expected data: abi.encode(bytes receiver,uint256 minReceiveAmount,string receiverChainName); | + +### _depositAndSend + +```solidity +function _depositAndSend(bytes _depositor, uint256 _paymentTokenAmount, bytes _data) internal +``` + +internal function to deposit and send the paymentToken to the destination chain + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _depositor | bytes | the depositor of the operation | +| _paymentTokenAmount | uint256 | the amount of payment tokens to deposit | +| _data | bytes | the data of the operation | + +### _redeemAndSend + +```solidity +function _redeemAndSend(bytes _redeemer, uint256 _mTokenAmount, bytes _data) internal +``` + +internal function to redeem and send the mToken to the destination chain + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _redeemer | bytes | the address of the redeemer | +| _mTokenAmount | uint256 | the amount of mTokens to redeem | +| _data | bytes | the data of the operation | + +### _deposit + +```solidity +function _deposit(address _receiver, uint256 _paymentTokenAmount, uint256 _minReceiveAmount, bytes32 _referrerId) internal returns (uint256 mTokenAmount) +``` + +function to deposit into Midas vault + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _receiver | address | the address to receive the mTokens | +| _paymentTokenAmount | uint256 | the amount of paymentToken to deposit | +| _minReceiveAmount | uint256 | the minimum amount of mTokens to receive | +| _referrerId | bytes32 | the referrer id for the user | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| mTokenAmount | uint256 | the amount of mTokens received | + +### _redeem + +```solidity +function _redeem(address _receiver, uint256 _mTokenAmount, uint256 _minReceiveAmount) internal returns (uint256 paymentTokenAmount) +``` + +function to redeem from Midas vault + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _receiver | address | the address to receive the paymentToken | +| _mTokenAmount | uint256 | the amount of mTokens to redeem | +| _minReceiveAmount | uint256 | the minimum amount of paymentToken to receive | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| paymentTokenAmount | uint256 | the amount of paymentToken received | + +### _balanceOf + +```solidity +function _balanceOf(address _token, address _of) internal view returns (uint256) +``` + +function to get the balance of a token + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _token | address | the address of the token | +| _of | address | the address of the account | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | the balance of the token | + +### _itsTransfer + +```solidity +function _itsTransfer(string destinationChain, bytes destinationAddress, bytes32 tokenId, uint256 amount, uint256 gasValue) internal +``` + +internal function to transfer the token using ITS + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| destinationChain | string | the destination chain | +| destinationAddress | bytes | the destination address | +| tokenId | bytes32 | the ITS tokenId | +| amount | uint256 | the amount of the token | +| gasValue | uint256 | the gas value to be paid for the transfer | + +### _bytesToAddress + +```solidity +function _bytesToAddress(bytes b) internal pure returns (address addr) +``` + +internal function to convert a bytes to an address + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| b | bytes | bytes value encode using `abi.encodePacked(address)` | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| addr | address | the address | + +### _tokenAmountToBase18 + +```solidity +function _tokenAmountToBase18(uint256 amount) internal view returns (uint256) +``` + +internal function to convert a token amount to base18 + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| amount | uint256 | the amount of the token | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | the amount in base18 | + +## IMidasAxelarVaultExecutable + +Interface for the MidasAxelarVaultExecutable contract + +### Sent + +```solidity +event Sent(bytes32 commandId) +``` + +event emitted when a operation is successful + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| commandId | bytes32 | the commandId of the send operation | + +### Refunded + +```solidity +event Refunded(bytes32 commandId, bytes _error) +``` + +event emitted when a refund operation is successful + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| commandId | bytes32 | the commandId of the refund operation | +| _error | bytes | | + +### Deposited + +```solidity +event Deposited(bytes sender, bytes recipient, string destinationChain, uint256 paymentTokenAmount, uint256 mTokenAmount) +``` + +event emitted when a deposit operation is successful + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| sender | bytes | the sender of the deposit operation | +| recipient | bytes | the recipient of the deposit operation | +| destinationChain | string | the destination chain of the deposit operation | +| paymentTokenAmount | uint256 | the amount of payment tokens deposited | +| mTokenAmount | uint256 | the amount of m tokens deposited | + +### Redeemed + +```solidity +event Redeemed(bytes sender, bytes recipient, string destinationChain, uint256 mTokenAmount, uint256 paymentTokenAmount) +``` + +event emitted when a redemption operation is successful + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| sender | bytes | the sender of the redemption operation | +| recipient | bytes | the recipient of the redemption operation | +| destinationChain | string | the destination chain of the redemption operation | +| mTokenAmount | uint256 | the amount of m tokens redeemed | +| paymentTokenAmount | uint256 | the amount of payment tokens redeemed | + +### OnlySelf + +```solidity +error OnlySelf(address caller) +``` + +error emitted when the caller is not the self + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | the caller of the function | + +### OnlyValidExecutableTokenId + +```solidity +error OnlyValidExecutableTokenId(bytes32 tokenId) +``` + +error emitted when the tokenId is not a valid executable tokenId + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenId | bytes32 | the tokenId of the ITS token | + +### depositVault + +```solidity +function depositVault() external view returns (contract IDepositVault) +``` + +getter for the deposit vault + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | contract IDepositVault | the deposit vault | + +### redemptionVault + +```solidity +function redemptionVault() external view returns (contract IRedemptionVault) +``` + +getter for the redemption vault + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | contract IRedemptionVault | the redemption vault | + +### paymentTokenId + +```solidity +function paymentTokenId() external view returns (bytes32) +``` + +getter for the paymentToken ITS id + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | the paymentToken ITS id | + +### paymentTokenErc20 + +```solidity +function paymentTokenErc20() external view returns (address) +``` + +getter for the paymentToken ERC20 + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | address | the paymentToken ERC20 | + +### mTokenId + +```solidity +function mTokenId() external view returns (bytes32) +``` + +getter for the mToken ITS id + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | the mToken ITS id | + +### mTokenErc20 + +```solidity +function mTokenErc20() external view returns (address) +``` + +getter for the mToken ERC20 + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | address | the mToken ERC20 | + +### depositAndSend + +```solidity +function depositAndSend(uint256 _paymentTokenAmount, bytes _data) external payable +``` + +deposits and sends the paymentToken to the destination chain + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _paymentTokenAmount | uint256 | the amount of payment tokens to deposit | +| _data | bytes | encoded data for the deposit. Expected data: abi.encode(bytes receiver,uint256 minReceiveAmount,bytes32 referrerId,string receiverChainName); | + +### redeemAndSend + +```solidity +function redeemAndSend(uint256 _mTokenAmount, bytes _data) external payable +``` + +redeems and sends the mToken to the destination chain + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _mTokenAmount | uint256 | the amount of m tokens to redeem | +| _data | bytes | encoded data for the redemption Expected data: abi.encode(bytes receiver,uint256 minReceiveAmount,string receiverChainName); | + +## MidasLzVaultComposerSync + +This contract is a composer that allows deposits and redemptions operations against a + synchronous vault across different chains using LayerZero's OFT protocol. + +_The contract is designed to handle deposits and redemptions of vault mTokens and paymentTokens, + ensuring that the mToken and paymentToken are correctly managed and transferred across chains. + It also includes slippage protection and refund mechanisms for failed transactions. +Default refunds are enabled to EOA addresses only on the source._ + +### TokenAddressMismatch + +```solidity +error TokenAddressMismatch(address oftTokenValue, address dvValue, address rvValue) +``` + +error for token address mismatch + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| oftTokenValue | address | address of OFT token | +| dvValue | address | address of mToken of deposit vault | +| rvValue | address | address of mToken of redemption vault | + +### InvalidTokenRate + +```solidity +error InvalidTokenRate(address feed) +``` + +error for invalid token rate + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| feed | address | address of failed data feed contract | + +### depositVault + +```solidity +contract IDepositVault depositVault +``` + +getter for the deposit vault + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### redemptionVault + +```solidity +contract IRedemptionVault redemptionVault +``` + +getter for the redemption vault + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### paymentTokenOft + +```solidity +address paymentTokenOft +``` + +getter for the paymentToken OFT + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### paymentTokenErc20 + +```solidity +address paymentTokenErc20 +``` + +getter for the paymentToken ERC20 + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### mTokenOft + +```solidity +address mTokenOft +``` + +getter for the mToken OFT + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### mTokenErc20 + +```solidity +address mTokenErc20 +``` + +getter for the mToken ERC20 + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### paymentTokenDecimals + +```solidity +uint8 paymentTokenDecimals +``` + +decimals of `paymentTokenErc20` + +### lzEndpoint + +```solidity +address lzEndpoint +``` + +getter for the LayerZero endpoint + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### thisChaindEid + +```solidity +uint32 thisChaindEid +``` + +getter for the current chain EID + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### constructor + +```solidity +constructor(address _depositVault, address _redemptionVault, address _paymentTokenOft, address _mTokenOft) public +``` + +### initialize + +```solidity +function initialize() external +``` + +Initializes the contract + +### lzCompose + +```solidity +function lzCompose(address _composeSender, bytes32 _guid, bytes _message, address, bytes) external payable +``` + +Handles LayerZero compose operations for vault transactions with automatic refund functionality + +_This composer is designed to handle refunds to an EOA address and not a contract +Any revert in handleCompose() causes a refund back to the src EXCEPT for InsufficientMsgValue_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _composeSender | address | The OFT contract address used for refunds, must be either paymentTokenOft or mTokenOft | +| _guid | bytes32 | LayerZero's unique tx id (created on the source tx) | +| _message | bytes | Decomposable bytes object into [composeHeader][composeMessage] | +| | address | | +| | bytes | | + +### handleCompose + +```solidity +function handleCompose(address _oftIn, bytes32 _composeFrom, bytes _composeMsg, uint256 _amount) public payable virtual +``` + +Handles the compose operation for OFT transactions + +_This function can only be called by the contract itself (self-call restriction) + Decodes the compose message to extract SendParam and minimum message value + Routes to either deposit or redeem flow based on the input OFT token type_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _oftIn | address | The OFT token whose funds have been received in the lzReceive associated with this lzTx | +| _composeFrom | bytes32 | The bytes32 identifier of the compose sender | +| _composeMsg | bytes | The encoded message containing SendParam, minMsgValue and extraOptions | +| _amount | uint256 | The amount of tokens received in the lzReceive associated with this lzTx | + +### depositAndSend + +```solidity +function depositAndSend(uint256 _paymentTokenAmount, bytes _extraOptions, struct SendParam _sendParam, address _refundAddress) external payable +``` + +Deposits payment token from the caller into the vault and sends them to the recipient + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _paymentTokenAmount | uint256 | | +| _extraOptions | bytes | | +| _sendParam | struct SendParam | | +| _refundAddress | address | | + +### redeemAndSend + +```solidity +function redeemAndSend(uint256 _mTokenAmount, bytes _extraOptions, struct SendParam _sendParam, address _refundAddress) external payable +``` + +Redeems vault mTokens and sends the resulting payment tokens to the user + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _mTokenAmount | uint256 | | +| _extraOptions | bytes | | +| _sendParam | struct SendParam | | +| _refundAddress | address | | + +### _depositAndSend + +```solidity +function _depositAndSend(bytes32 _depositor, uint256 _paymentTokenAmount, bytes _extraOptions, struct SendParam _sendParam, address _refundAddress) internal +``` + +This function first deposits the paymentTokens to mint mTokens, validates the mTokens meet minimum slippage requirements, + then sends the minted mTokens cross-chain using the OFT protocol +The _sendParam.amountLD is updated to the actual mToken amount minted, and minAmountLD is reset to 0 for the send operation + +_Internal function that deposits paymentTokens and sends mTokens to another chain_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _depositor | bytes32 | The depositor (bytes32 format to account for non-evm addresses) | +| _paymentTokenAmount | uint256 | The number of paymentTokens to deposit | +| _extraOptions | bytes | Extra options for the deposit operation | +| _sendParam | struct SendParam | Parameter that defines how to send the mTokens | +| _refundAddress | address | Address to receive excess payment of the LZ fees | + +### _redeemAndSend + +```solidity +function _redeemAndSend(bytes32 _redeemer, uint256 _mTokenAmount, bytes, struct SendParam _sendParam, address _refundAddress) internal +``` + +This function first redeems the specified mToken amount for the underlying paymentToken, + validates the received amount against slippage protection, then initiates a cross-chain + transfer of the redeemed paymentTokens using the OFT protocol +The minAmountLD in _sendParam is reset to 0 after slippage validation since the + actual amount has already been verified + +_Internal function that redeems mTokens for paymentTokens and sends them cross-chain_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _redeemer | bytes32 | The address of the redeemer in bytes32 format | +| _mTokenAmount | uint256 | The number of mTokens to redeem | +| | bytes | | +| _sendParam | struct SendParam | Parameter that defines how to send the paymentTokens | +| _refundAddress | address | Address to receive excess payment of the LZ fees | + +### _deposit + +```solidity +function _deposit(address _receiver, uint256 _paymentTokenAmount, uint256 _minReceiveAmount, bytes32 _referrerId) internal returns (uint256 mTokenAmount) +``` + +_Internal function to deposit paymentTokens into the vault_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _receiver | address | The address to receive the mTokens | +| _paymentTokenAmount | uint256 | The number of paymentTokens to deposit into the vault | +| _minReceiveAmount | uint256 | The minimum amount of mTokens to receive | +| _referrerId | bytes32 | The referrer id | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| mTokenAmount | uint256 | The number of mTokens received from the vault deposit | + +### _redeem + +```solidity +function _redeem(address _receiver, uint256 _mTokenAmount, uint256 _minReceiveAmount) internal returns (uint256 paymentTokenAmount) +``` + +_Internal function to redeem mTokens from the vault_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _receiver | address | The address to receive the paymentTokens | +| _mTokenAmount | uint256 | The number of mTokens to redeem from the vault | +| _minReceiveAmount | uint256 | The minimum amount of paymentTokens to receive | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| paymentTokenAmount | uint256 | The number of paymentTokens received from the vault redemption | + +### _sendOft + +```solidity +function _sendOft(address _oft, struct SendParam _sendParam, address _refundAddress) internal +``` + +_Internal function that handles token transfer to the recipient +If the destination eid is the same as the current eid, it transfers the tokens directly to the recipient +If the destination eid is different, it sends a LayerZero cross-chain transaction_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _oft | address | The OFT contract address to use for sending | +| _sendParam | struct SendParam | The parameters for the send operation | +| _refundAddress | address | Address to receive excess payment of the LZ fees | + +### _refund + +```solidity +function _refund(address _oft, bytes _message, uint256 _amount, address _refundAddress) internal +``` + +_Internal function to refund input tokens to sender on source during a failed transaction_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _oft | address | The OFT contract address used for refunding | +| _message | bytes | The original message that was sent | +| _amount | uint256 | The amount of tokens to refund | +| _refundAddress | address | Address to receive the refund | + +### _requireNoValue + +```solidity +function _requireNoValue() internal view +``` + +_Internal function to revert if msg.value is not 0_ + +### _parseDepositExtraOptions + +```solidity +function _parseDepositExtraOptions(bytes _extraOptions) internal pure returns (bytes32 referrerId) +``` + +_Internal function to parse the extra options_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _extraOptions | bytes | The extra options for the deposit operation | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| referrerId | bytes32 | The referrer id | + +### _balanceOf + +```solidity +function _balanceOf(address _token, address _of) internal view returns (uint256) +``` + +_Internal function to get the balance of the token of the contract_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _token | address | the address of the token | +| _of | address | the address of the account | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | balance The balance of the token of the contract | + +### _tokenAmountToBase18 + +```solidity +function _tokenAmountToBase18(uint256 amount) internal view returns (uint256) +``` + +_Internal function to convert a token amount to base18_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| amount | uint256 | The amount of the token | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | The amount in base18 | + +### receive + +```solidity +receive() external payable +``` + +========================== Receive ===================================== + +## IMidasLzVaultComposerSync + +Interface for the MidasLzVaultComposerSync contract + +### Sent + +```solidity +event Sent(bytes32 guid) +``` + +event emitted when a send operation is successful + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| guid | bytes32 | the guid of the send operation | + +### Refunded + +```solidity +event Refunded(bytes32 guid) +``` + +event emitted when a refund operation is successful + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| guid | bytes32 | the guid of the refund operation | + +### Deposited + +```solidity +event Deposited(bytes32 sender, bytes32 recipient, uint32 dstEid, uint256 paymentTokenAmount, uint256 mTokenAmount) +``` + +event emitted when a deposit operation is successful + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| sender | bytes32 | the sender of the deposit operation | +| recipient | bytes32 | the recipient of the deposit operation | +| dstEid | uint32 | the destination eid of the deposit operation | +| paymentTokenAmount | uint256 | the amount of payment tokens deposited | +| mTokenAmount | uint256 | the amount of m tokens deposited | + +### Redeemed + +```solidity +event Redeemed(bytes32 sender, bytes32 recipient, uint32 dstEid, uint256 mTokenAmount, uint256 paymentTokenAmount) +``` + +event emitted when a redemption operation is successful + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| sender | bytes32 | the sender of the redemption operation | +| recipient | bytes32 | the recipient of the redemption operation | +| dstEid | uint32 | the destination eid of the redemption operation | +| mTokenAmount | uint256 | the amount of m tokens redeemed | +| paymentTokenAmount | uint256 | the amount of payment tokens redeemed | + +### OnlyEndpoint + +```solidity +error OnlyEndpoint(address caller) +``` + +error emitted when the caller is not the endpoint + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | the caller of the function | + +### OnlySelf + +```solidity +error OnlySelf(address caller) +``` + +error emitted when the caller is not the self + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | the caller of the function | + +### OnlyValidComposeCaller + +```solidity +error OnlyValidComposeCaller(address caller) +``` + +error emitted when the caller is not a valid compose caller + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | the caller of the function | + +### InsufficientMsgValue + +```solidity +error InsufficientMsgValue(uint256 expectedMsgValue, uint256 actualMsgValue) +``` + +error emitted when the msg.value is insufficient + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| expectedMsgValue | uint256 | the expected msg.value | +| actualMsgValue | uint256 | the actual msg.value | + +### NoMsgValueExpected + +```solidity +error NoMsgValueExpected() +``` + +error emitted when msg.value expected to be 0 but is not + +### depositVault + +```solidity +function depositVault() external view returns (contract IDepositVault) +``` + +getter for the deposit vault + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | contract IDepositVault | the deposit vault | + +### redemptionVault + +```solidity +function redemptionVault() external view returns (contract IRedemptionVault) +``` + +getter for the redemption vault + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | contract IRedemptionVault | the redemption vault | + +### paymentTokenOft + +```solidity +function paymentTokenOft() external view returns (address) +``` + +getter for the paymentToken OFT + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | address | the paymentToken OFT | + +### paymentTokenErc20 + +```solidity +function paymentTokenErc20() external view returns (address) +``` + +getter for the paymentToken ERC20 + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | address | the paymentToken ERC20 | + +### mTokenOft + +```solidity +function mTokenOft() external view returns (address) +``` + +getter for the mToken OFT + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | address | the mToken OFT | + +### mTokenErc20 + +```solidity +function mTokenErc20() external view returns (address) +``` + +getter for the mToken ERC20 + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | address | the mToken ERC20 | + +### lzEndpoint + +```solidity +function lzEndpoint() external view returns (address) +``` + +getter for the LayerZero endpoint + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | address | the LayerZero endpoint | + +### thisChaindEid + +```solidity +function thisChaindEid() external view returns (uint32) +``` + +getter for the current chain EID + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint32 | the current chain EID | + +### depositAndSend + +```solidity +function depositAndSend(uint256 paymentTokenAmount, bytes extraOptions, struct SendParam sendParam, address refundAddress) external payable +``` + +Deposits payment token from the caller into the vault and sends them to the recipient + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| paymentTokenAmount | uint256 | The number of ERC20 tokens to deposit and send | +| extraOptions | bytes | Extra options for the deposit operation. Expected extraOptions: abi.encode(bytes32 referrerId) or 0x | +| sendParam | struct SendParam | Parameters on how to send the mTokens to the recipient | +| refundAddress | address | Address to receive excess `msg.value` | + +### redeemAndSend + +```solidity +function redeemAndSend(uint256 mTokenAmount, bytes extraOptions, struct SendParam sendParam, address refundAddress) external payable +``` + +Redeems vault mTokens and sends the resulting payment tokens to the user + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| mTokenAmount | uint256 | The number of vault mTokens to redeem | +| extraOptions | bytes | Extra options for the redeem operation. Expected extraOptions: 0x | +| sendParam | struct SendParam | Parameter that defines how to send the payment tokens to the recipient | +| refundAddress | address | Address to receive excess payment of the LZ fees | + +### receive + +```solidity +receive() external payable +``` + +========================== Receive ===================================== + +## JivDepositVault + +Smart contract that handles JIV minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## JivMidasAccessControlRoles + +Base contract that stores all roles descriptors for JIV contracts + +### JIV_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 JIV_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage JivDepositVault + +### JIV_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 JIV_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage JivRedemptionVault + +### JIV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 JIV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage JivCustomAggregatorFeed and JivDataFeed + +## AcreMBtc1DepositVault + +Smart contract that handles acremBTC1 minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## AcreMBtc1MidasAccessControlRoles + +Base contract that stores all roles descriptors for acremBTC1 contracts + +### ACRE_BTC_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 ACRE_BTC_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage AcreMBtc1DepositVault + +### ACRE_BTC_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 ACRE_BTC_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage AcreMBtc1RedemptionVault + +### ACRE_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 ACRE_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage AcreMBtc1CustomAggregatorFeed and AcreMBtc1DataFeed + +## CUsdoDepositVault + +Smart contract that handles cUSDO minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## CUsdoMidasAccessControlRoles + +Base contract that stores all roles descriptors for cUSDO contracts + +### C_USDO_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 C_USDO_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage CUsdoDepositVault + +### C_USDO_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 C_USDO_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage CUsdoRedemptionVault + +### C_USDO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 C_USDO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage CUsdoCustomAggregatorFeed and CUsdoDataFeed + +## DnEthDepositVault + +Smart contract that handles dnETH minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## DnEthMidasAccessControlRoles + +Base contract that stores all roles descriptors for dnETH contracts + +### DN_ETH_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 DN_ETH_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage DnEthDepositVault + +### DN_ETH_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 DN_ETH_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage DnEthRedemptionVault + +### DN_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 DN_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage DnEthCustomAggregatorFeed and DnEthDataFeed + +## DnFartDepositVault + +Smart contract that handles dnFART minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## DnFartMidasAccessControlRoles + +Base contract that stores all roles descriptors for dnFART contracts + +### DN_FART_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 DN_FART_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage DnFartDepositVault + +### DN_FART_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 DN_FART_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage DnFartRedemptionVault + +### DN_FART_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 DN_FART_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage DnFartCustomAggregatorFeed and DnFartDataFeed + +## DnHypeDepositVault + +Smart contract that handles dnHYPE minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## DnHypeMidasAccessControlRoles + +Base contract that stores all roles descriptors for dnHYPE contracts + +### DN_HYPE_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 DN_HYPE_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage DnHypeDepositVault + +### DN_HYPE_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 DN_HYPE_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage DnHypeRedemptionVault + +### DN_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 DN_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage DnHypeCustomAggregatorFeed and DnHypeDataFeed + +## DnPumpDepositVault + +Smart contract that handles dnPUMP minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## DnPumpMidasAccessControlRoles + +Base contract that stores all roles descriptors for dnPUMP contracts + +### DN_PUMP_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 DN_PUMP_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage DnPumpDepositVault + +### DN_PUMP_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 DN_PUMP_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage DnPumpRedemptionVault + +### DN_PUMP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 DN_PUMP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage DnPumpCustomAggregatorFeed and DnPumpDataFeed + +## DnTestDepositVault + +Smart contract that handles dnTEST minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## DnTestMidasAccessControlRoles + +Base contract that stores all roles descriptors for dnTEST contracts + +### DN_TEST_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 DN_TEST_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage DnTestDepositVault + +### DN_TEST_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 DN_TEST_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage DnTestRedemptionVault + +### DN_TEST_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 DN_TEST_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage DnTestCustomAggregatorFeed and DnTestDataFeed + +## EUsdDepositVault + +Smart contract that handles eUSD minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +### greenlistedRole + +```solidity +function greenlistedRole() public pure returns (bytes32) +``` + +AC role of a greenlist + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role bytes32 role | + +## EUsdMidasAccessControlRoles + +Base contract that stores all roles descriptors for eUSD contracts + +### E_USD_VAULT_ROLES_OPERATOR + +```solidity +bytes32 E_USD_VAULT_ROLES_OPERATOR +``` + +actor that can manage vault admin roles + +### E_USD_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 E_USD_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage EUsdDepositVault + +### E_USD_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 E_USD_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage EUsdRedemptionVault + +### E_USD_GREENLIST_OPERATOR_ROLE + +```solidity +bytes32 E_USD_GREENLIST_OPERATOR_ROLE +``` + +actor that can change eUSD green list statuses of addresses + +### E_USD_GREENLISTED_ROLE + +```solidity +bytes32 E_USD_GREENLISTED_ROLE +``` + +actor that is greenlisted in eUSD + +## HBUsdcDepositVault + +Smart contract that handles hbUSDC minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## HBUsdcMidasAccessControlRoles + +Base contract that stores all roles descriptors for hbUSDC contracts + +### HB_USDC_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 HB_USDC_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage HBUsdcDepositVault + +### HB_USDC_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 HB_USDC_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage HBUsdcRedemptionVault + +### HB_USDC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 HB_USDC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage HBUsdcCustomAggregatorFeed and HBUsdcDataFeed + +## HBUsdtDepositVault + +Smart contract that handles hbUSDT minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## HBUsdtMidasAccessControlRoles + +Base contract that stores all roles descriptors for hbUSDT contracts + +### HB_USDT_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 HB_USDT_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage HBUsdtDepositVault + +### HB_USDT_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 HB_USDT_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage HBUsdtRedemptionVault + +### HB_USDT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 HB_USDT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage HBUsdtCustomAggregatorFeed and HBUsdtDataFeed + +## HBXautDepositVault + +Smart contract that handles hbXAUt minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## HBXautMidasAccessControlRoles + +Base contract that stores all roles descriptors for hbXAUt contracts + +### HB_XAUT_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 HB_XAUT_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage HBXautDepositVault + +### HB_XAUT_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 HB_XAUT_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage HBXautRedemptionVault + +### HB_XAUT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 HB_XAUT_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage HBXautCustomAggregatorFeed and HBXautDataFeed + +## HypeBtcDepositVault + +Smart contract that handles hypeBTC minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## HypeBtcMidasAccessControlRoles + +Base contract that stores all roles descriptors for hypeBTC contracts + +### HYPE_BTC_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 HYPE_BTC_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage HypeBtcDepositVault + +### HYPE_BTC_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 HYPE_BTC_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage HypeBtcRedemptionVault + +### HYPE_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 HYPE_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage HypeBtcCustomAggregatorFeed and HypeBtcDataFeed + +## HypeEthDepositVault + +Smart contract that handles hypeETH minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## HypeEthMidasAccessControlRoles + +Base contract that stores all roles descriptors for hypeETH contracts + +### HYPE_ETH_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 HYPE_ETH_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage HypeEthDepositVault + +### HYPE_ETH_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 HYPE_ETH_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage HypeEthRedemptionVault + +### HYPE_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 HYPE_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage HypeEthCustomAggregatorFeed and HypeEthDataFeed + +## HypeUsdDepositVault + +Smart contract that handles hypeUSD minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## HypeUsdMidasAccessControlRoles + +Base contract that stores all roles descriptors for hypeUSD contracts + +### HYPE_USD_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 HYPE_USD_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage HypeUsdDepositVault + +### HYPE_USD_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 HYPE_USD_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage HypeUsdRedemptionVault + +### HYPE_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 HYPE_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage HypeUsdCustomAggregatorFeed and HypeUsdDataFeed + +## KitBtcDepositVault + +Smart contract that handles kitBTC minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## KitBtcMidasAccessControlRoles + +Base contract that stores all roles descriptors for kitBTC contracts + +### KIT_BTC_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 KIT_BTC_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage KitBtcDepositVault + +### KIT_BTC_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 KIT_BTC_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage KitBtcRedemptionVault + +### KIT_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 KIT_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage KitBtcCustomAggregatorFeed and KitBtcDataFeed + +## KitHypeDepositVault + +Smart contract that handles kitHYPE minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## KitHypeMidasAccessControlRoles + +Base contract that stores all roles descriptors for kitHYPE contracts + +### KIT_HYPE_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 KIT_HYPE_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage KitHypeDepositVault + +### KIT_HYPE_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 KIT_HYPE_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage KitHypeRedemptionVault + +### KIT_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 KIT_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage KitHypeCustomAggregatorFeed and KitHypeDataFeed + +## KitUsdDepositVault + +Smart contract that handles kitUSD minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## KitUsdMidasAccessControlRoles + +Base contract that stores all roles descriptors for kitUSD contracts + +### KIT_USD_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 KIT_USD_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage KitUsdDepositVault + +### KIT_USD_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 KIT_USD_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage KitUsdRedemptionVault + +### KIT_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 KIT_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage KitUsdCustomAggregatorFeed and KitUsdDataFeed + +## KmiUsdDepositVault + +Smart contract that handles kmiUSD minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## KmiUsdMidasAccessControlRoles + +Base contract that stores all roles descriptors for kmiUSD contracts + +### KMI_USD_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 KMI_USD_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage KmiUsdDepositVault + +### KMI_USD_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 KMI_USD_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage KmiUsdRedemptionVault + +### KMI_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 KMI_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage KmiUsdCustomAggregatorFeed and KmiUsdDataFeed + +## LiquidHypeDepositVault + +Smart contract that handles liquidHYPE minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## LiquidHypeMidasAccessControlRoles + +Base contract that stores all roles descriptors for liquidHYPE contracts + +### LIQUID_HYPE_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 LIQUID_HYPE_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage LiquidHypeDepositVault + +### LIQUID_HYPE_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 LIQUID_HYPE_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage LiquidHypeRedemptionVault + +### LIQUID_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 LIQUID_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage LiquidHypeCustomAggregatorFeed and LiquidHypeDataFeed + +## LiquidReserveDepositVault + +Smart contract that handles liquidRESERVE minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## LiquidReserveMidasAccessControlRoles + +Base contract that stores all roles descriptors for liquidRESERVE contracts + +### LIQUID_RESERVE_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 LIQUID_RESERVE_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage LiquidReserveDepositVault + +### LIQUID_RESERVE_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 LIQUID_RESERVE_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage LiquidReserveRedemptionVault + +### LIQUID_RESERVE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 LIQUID_RESERVE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage LiquidReserveCustomAggregatorFeed and LiquidReserveDataFeed + +## LstHypeDepositVault + +Smart contract that handles LstHype minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## LstHypeMidasAccessControlRoles + +Base contract that stores all roles descriptors for lstHYPE contracts + +### LST_HYPE_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 LST_HYPE_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage LstHypeDepositVault + +### LST_HYPE_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 LST_HYPE_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage LstHypeRedemptionVault + +### LST_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 LST_HYPE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage LstHypeCustomAggregatorFeed and LstHypeDataFeed + +## MApolloDepositVault + +Smart contract that handles mAPOLLO minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MApolloMidasAccessControlRoles + +Base contract that stores all roles descriptors for mAPOLLO contracts + +### M_APOLLO_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_APOLLO_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MApolloDepositVault + +### M_APOLLO_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_APOLLO_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MApolloRedemptionVault + +### M_APOLLO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_APOLLO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MApolloCustomAggregatorFeed and MApolloDataFeed + +## MBasisDepositVault + +Smart contract that handles mBASIS minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MBasisMidasAccessControlRoles + +Base contract that stores all roles descriptors for mBASIS contracts + +### M_BASIS_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_BASIS_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MBasisDepositVault + +### M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MBasisRedemptionVault + +### M_BASIS_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_BASIS_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MBasisCustomAggregatorFeed and MBasisDataFeed + +## MBtcDepositVault + +Smart contract that handles mBTC minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MBtcMidasAccessControlRoles + +Base contract that stores all roles descriptors for mBTC contracts + +### M_BTC_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_BTC_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MBtcDepositVault + +### M_BTC_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_BTC_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MBtcRedemptionVault + +### M_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MBtcCustomAggregatorFeed and MBtcDataFeed + +## TACmBtcDepositVault + +Smart contract that handles TACmBTC minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## TACmBtcMidasAccessControlRoles + +Base contract that stores all roles descriptors for TACmBTC contracts + +### TAC_M_BTC_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 TAC_M_BTC_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage TACmBtcDepositVault + +### TAC_M_BTC_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 TAC_M_BTC_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage TACmBtcRedemptionVault + +## MEdgeDepositVault + +Smart contract that handles mEDGE minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MEdgeMidasAccessControlRoles + +Base contract that stores all roles descriptors for mEDGE contracts + +### M_EDGE_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_EDGE_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MEdgeDepositVault + +### M_EDGE_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_EDGE_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MEdgeRedemptionVault + +### M_EDGE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_EDGE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MEdgeCustomAggregatorFeed and MEdgeDataFeed + +## TACmEdgeDepositVault + +Smart contract that handles TACmEdge minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## TACmEdgeMidasAccessControlRoles + +Base contract that stores all roles descriptors for TACmEdge contracts + +### TAC_M_EDGE_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 TAC_M_EDGE_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage TACmEdgeDepositVault + +### TAC_M_EDGE_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 TAC_M_EDGE_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage TACmEdgeRedemptionVault + +## MEvUsdDepositVault + +Smart contract that handles mEVUSD minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MEvUsdMidasAccessControlRoles + +Base contract that stores all roles descriptors for mEVUSD contracts + +### M_EV_USD_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_EV_USD_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MEvUsdDepositVault + +### M_EV_USD_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_EV_USD_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MEvUsdRedemptionVault + +### M_EV_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_EV_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MEvUsdCustomAggregatorFeed and MEvUsdDataFeed + +## MFarmDepositVault + +Smart contract that handles mFARM minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MFarmMidasAccessControlRoles + +Base contract that stores all roles descriptors for mFARM contracts + +### M_FARM_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_FARM_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MFarmDepositVault + +### M_FARM_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_FARM_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MFarmRedemptionVault + +### M_FARM_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_FARM_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MFarmCustomAggregatorFeed and MFarmDataFeed + +## MFOneDepositVault + +Smart contract that handles mF-ONE minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MFOneMidasAccessControlRoles + +Base contract that stores all roles descriptors for mF-ONE contracts + +### M_FONE_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_FONE_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MFOneDepositVault + +### M_FONE_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_FONE_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MFOneRedemptionVault + +### M_FONE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_FONE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MFOneCustomAggregatorFeed and MFOneDataFeed + +## MHyperDepositVault + +Smart contract that handles mHYPER minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MHyperMidasAccessControlRoles + +Base contract that stores all roles descriptors for mHYPER contracts + +### M_HYPER_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_HYPER_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MHyperDepositVault + +### M_HYPER_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_HYPER_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MHyperRedemptionVault + +### M_HYPER_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_HYPER_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MHyperCustomAggregatorFeed and MHyperDataFeed + +## MHyperBtcDepositVault + +Smart contract that handles mHyperBTC minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MHyperBtcMidasAccessControlRoles + +Base contract that stores all roles descriptors for mHyperBTC contracts + +### M_HYPER_BTC_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_HYPER_BTC_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MHyperBtcDepositVault + +### M_HYPER_BTC_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_HYPER_BTC_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MHyperBtcRedemptionVault + +### M_HYPER_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_HYPER_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MHyperBtcCustomAggregatorFeed and MHyperBtcDataFeed + +## MHyperEthDepositVault + +Smart contract that handles mHyperETH minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MHyperEthMidasAccessControlRoles + +Base contract that stores all roles descriptors for mHyperETH contracts + +### M_HYPER_ETH_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_HYPER_ETH_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MHyperEthDepositVault + +### M_HYPER_ETH_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_HYPER_ETH_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MHyperEthRedemptionVault + +### M_HYPER_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_HYPER_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MHyperEthCustomAggregatorFeed and MHyperEthDataFeed + +## MKRalphaDepositVault + +Smart contract that handles mKRalpha minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MKRalphaMidasAccessControlRoles + +Base contract that stores all roles descriptors for mKRalpha contracts + +### M_KRALPHA_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_KRALPHA_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MKRalphaDepositVault + +### M_KRALPHA_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_KRALPHA_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MKRalphaRedemptionVault + +### M_KRALPHA_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_KRALPHA_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MKRalphaCustomAggregatorFeed and MKRalphaDataFeed + +## MLiquidityDepositVault + +Smart contract that handles mLIQUIDITY minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MLiquidityMidasAccessControlRoles + +Base contract that stores all roles descriptors for mLIQUIDITY contracts + +### M_LIQUIDITY_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_LIQUIDITY_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MLiquidityDepositVault + +### M_LIQUIDITY_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_LIQUIDITY_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MLiquidityRedemptionVault + +### M_LIQUIDITY_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_LIQUIDITY_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MLiquidityCustomAggregatorFeed and MLiquidityDataFeed + +## MM1UsdDepositVault + +Smart contract that handles mM1USD minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MM1UsdMidasAccessControlRoles + +Base contract that stores all roles descriptors for mM1USD contracts + +### M_M1_USD_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_M1_USD_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MM1UsdDepositVault + +### M_M1_USD_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_M1_USD_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MM1UsdRedemptionVault + +### M_M1_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_M1_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MM1UsdCustomAggregatorFeed and MM1UsdDataFeed + +## MMevDepositVault + +Smart contract that handles mMEV minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MMevMidasAccessControlRoles + +Base contract that stores all roles descriptors for mMEV contracts + +### M_MEV_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_MEV_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MMevDepositVault + +### M_MEV_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_MEV_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MMevRedemptionVault + +### M_MEV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_MEV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MMevCustomAggregatorFeed and MMevDataFeed + +## TACmMevDepositVault + +Smart contract that handles TACmMEV minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## TACmMevMidasAccessControlRoles + +Base contract that stores all roles descriptors for TACmMEV contracts + +### TAC_M_MEV_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 TAC_M_MEV_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage TACmMevDepositVault + +### TAC_M_MEV_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 TAC_M_MEV_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage TACmMevRedemptionVault + +## MPortofinoDepositVault + +Smart contract that handles mPortofino minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MPortofinoMidasAccessControlRoles + +Base contract that stores all roles descriptors for mPortofino contracts + +### M_PORTOFINO_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_PORTOFINO_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MPortofinoDepositVault + +### M_PORTOFINO_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_PORTOFINO_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MPortofinoRedemptionVault + +### M_PORTOFINO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_PORTOFINO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MPortofinoCustomAggregatorFeed and MPortofinoDataFeed + +## MRe7DepositVault + +Smart contract that handles mRE7 minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MRe7MidasAccessControlRoles + +Base contract that stores all roles descriptors for mRE7 contracts + +### M_RE7_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_RE7_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MRe7DepositVault + +### M_RE7_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_RE7_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MRe7RedemptionVault ### M_RE7_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE ```solidity -bytes32 M_RE7_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +bytes32 M_RE7_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MRe7CustomAggregatorFeed and MRe7DataFeed + +## MRe7BtcDepositVault + +Smart contract that handles mRE7BTC minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MRe7BtcMidasAccessControlRoles + +Base contract that stores all roles descriptors for mRE7BTC contracts + +### M_RE7BTC_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_RE7BTC_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MRe7BtcDepositVault + +### M_RE7BTC_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_RE7BTC_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MRe7BtcRedemptionVault + +### M_RE7BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_RE7BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MRe7BtcCustomAggregatorFeed and MRe7BtcDataFeed + +## MRe7SolDepositVault + +Smart contract that handles mRE7SOL minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MRe7SolMidasAccessControlRoles + +Base contract that stores all roles descriptors for mRE7SOL contracts + +### M_RE7SOL_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_RE7SOL_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MRe7SolDepositVault + +### M_RE7SOL_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_RE7SOL_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MRe7SolRedemptionVault + +### M_RE7SOL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_RE7SOL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MRe7SolCustomAggregatorFeed and MRe7SolDataFeed + +## MRoxDepositVault + +Smart contract that handles mROX minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MRoxMidasAccessControlRoles + +Base contract that stores all roles descriptors for mROX contracts + +### M_ROX_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_ROX_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MRoxDepositVault + +### M_ROX_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_ROX_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MRoxRedemptionVault + +### M_ROX_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_ROX_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MRoxCustomAggregatorFeed and MRoxDataFeed + +## MSlDepositVault + +Smart contract that handles mSL minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MSlMidasAccessControlRoles + +Base contract that stores all roles descriptors for mSL contracts + +### M_SL_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_SL_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MSlDepositVault + +### M_SL_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_SL_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MSlRedemptionVault + +### M_SL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_SL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MSlCustomAggregatorFeed and MSlDataFeed + +## MTuDepositVault + +Smart contract that handles mTU minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MTuMidasAccessControlRoles + +Base contract that stores all roles descriptors for mTU contracts + +### M_TU_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_TU_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MTuDepositVault + +### M_TU_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_TU_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MTuRedemptionVault + +### M_TU_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_TU_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MTuCustomAggregatorFeed and MTuDataFeed + +## MWildUsdDepositVault + +Smart contract that handles mWildUSD minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MWildUsdMidasAccessControlRoles + +Base contract that stores all roles descriptors for mWildUSD contracts + +### M_WILD_USD_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_WILD_USD_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MWildUsdDepositVault + +### M_WILD_USD_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_WILD_USD_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MWildUsdRedemptionVault + +### M_WILD_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_WILD_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MWildUsdCustomAggregatorFeed and MWildUsdDataFeed + +## MXrpDepositVault + +Smart contract that handles mXRP minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MXrpMidasAccessControlRoles + +Base contract that stores all roles descriptors for mXRP contracts + +### M_XRP_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_XRP_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MXrpDepositVault + +### M_XRP_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_XRP_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MXrpRedemptionVault + +### M_XRP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_XRP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MXrpCustomAggregatorFeed and MXrpDataFeed + +## MevBtcDepositVault + +Smart contract that handles mevBTC minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MevBtcMidasAccessControlRoles + +Base contract that stores all roles descriptors for mevBTC contracts + +### MEV_BTC_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 MEV_BTC_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MevBtcDepositVault + +### MEV_BTC_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 MEV_BTC_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MevBtcRedemptionVault + +### MEV_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 MEV_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MevBtcCustomAggregatorFeed and MevBtcDataFeed + +## MSyrupUsdDepositVault + +Smart contract that handles msyrupUSD minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MSyrupUsdMidasAccessControlRoles + +Base contract that stores all roles descriptors for msyrupUSD contracts + +### M_SYRUP_USD_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_SYRUP_USD_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MSyrupUsdDepositVault + +### M_SYRUP_USD_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_SYRUP_USD_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MSyrupUsdRedemptionVault + +### M_SYRUP_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_SYRUP_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MSyrupUsdCustomAggregatorFeed and MSyrupUsdDataFeed + +## MSyrupUsdpDepositVault + +Smart contract that handles msyrupUSDp minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MSyrupUsdpMidasAccessControlRoles + +Base contract that stores all roles descriptors for msyrupUSDp contracts + +### M_SYRUP_USDP_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_SYRUP_USDP_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage MSyrupUsdpDepositVault + +### M_SYRUP_USDP_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 M_SYRUP_USDP_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage MSyrupUsdpRedemptionVault + +### M_SYRUP_USDP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 M_SYRUP_USDP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage MSyrupUsdpCustomAggregatorFeed and MSyrupUsdpDataFeed + +## ObeatUsdDepositVault + +Smart contract that handles obeatUSD minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## ObeatUsdMidasAccessControlRoles + +Base contract that stores all roles descriptors for obeatUSD contracts + +### OBEAT_USD_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 OBEAT_USD_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage ObeatUsdDepositVault + +### OBEAT_USD_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 OBEAT_USD_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage ObeatUsdRedemptionVault + +### OBEAT_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 OBEAT_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage ObeatUsdCustomAggregatorFeed and ObeatUsdDataFeed + +## PlUsdDepositVault + +Smart contract that handles plUSD minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## PlUsdMidasAccessControlRoles + +Base contract that stores all roles descriptors for plUSD contracts + +### PL_USD_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 PL_USD_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage PlUsdDepositVault + +### PL_USD_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 PL_USD_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage PlUsdRedemptionVault + +### PL_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 PL_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage PlUsdCustomAggregatorFeed and PlUsdDataFeed + +## SLInjDepositVault + +Smart contract that handles sLINJ minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## SLInjMidasAccessControlRoles + +Base contract that stores all roles descriptors for sLINJ contracts + +### SL_INJ_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 SL_INJ_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage SLInjDepositVault + +### SL_INJ_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 SL_INJ_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage SLInjRedemptionVault + +### SL_INJ_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 SL_INJ_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage SLInjCustomAggregatorFeed and SLInjDataFeed + +## SplUsdDepositVault + +Smart contract that handles splUSD minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## SplUsdMidasAccessControlRoles + +Base contract that stores all roles descriptors for splUSD contracts + +### SPL_USD_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 SPL_USD_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage SplUsdDepositVault + +### SPL_USD_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 SPL_USD_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage SplUsdRedemptionVault + +### SPL_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 SPL_USD_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage SplUsdCustomAggregatorFeed and SplUsdDataFeed + +## TBtcDepositVault + +Smart contract that handles tBTC minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## TBtcMidasAccessControlRoles + +Base contract that stores all roles descriptors for tBTC contracts + +### T_BTC_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 T_BTC_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage TBtcDepositVault + +### T_BTC_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 T_BTC_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage TBtcRedemptionVault + +### T_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 T_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage TBtcCustomAggregatorFeed and TBtcDataFeed + +## TEthDepositVault + +Smart contract that handles tETH minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## TEthMidasAccessControlRoles + +Base contract that stores all roles descriptors for tETH contracts + +### T_ETH_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 T_ETH_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage TEthDepositVault + +### T_ETH_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 T_ETH_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage TEthRedemptionVault + +### T_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 T_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage TEthCustomAggregatorFeed and TEthDataFeed + +## TUsdeDepositVault + +Smart contract that handles tUSDe minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## TUsdeMidasAccessControlRoles + +Base contract that stores all roles descriptors for tUSDe contracts + +### T_USDE_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 T_USDE_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage TUsdeDepositVault + +### T_USDE_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 T_USDE_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage TUsdeRedemptionVault + +### T_USDE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 T_USDE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage TUsdeCustomAggregatorFeed and TUsdeDataFeed + +## TacTonDepositVault + +Smart contract that handles tacTON minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## TacTonMidasAccessControlRoles + +Base contract that stores all roles descriptors for tacTON contracts + +### TAC_TON_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 TAC_TON_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage TacTonDepositVault + +### TAC_TON_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 TAC_TON_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage TacTonRedemptionVault + +### TAC_TON_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 TAC_TON_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage TacTonCustomAggregatorFeed and TacTonDataFeed + +## WNlpDepositVault + +Smart contract that handles wNLP minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## WNlpMidasAccessControlRoles + +Base contract that stores all roles descriptors for wNLP contracts + +### W_NLP_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 W_NLP_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage WNlpDepositVault + +### W_NLP_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 W_NLP_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage WNlpRedemptionVault + +### W_NLP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 W_NLP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage WNlpCustomAggregatorFeed and WNlpDataFeed + +## WVLPDepositVault + +Smart contract that handles wVLP minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## WVLPMidasAccessControlRoles + +Base contract that stores all roles descriptors for wVLP contracts + +### W_VLP_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 W_VLP_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage WVLPDepositVault + +### W_VLP_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 W_VLP_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage WVLPRedemptionVault + +### W_VLP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 W_VLP_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage WVLPCustomAggregatorFeed and WVLPDataFeed + +## WeEurDepositVault + +Smart contract that handles weEUR minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## WeEurMidasAccessControlRoles + +Base contract that stores all roles descriptors for weEUR contracts + +### WE_EUR_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 WE_EUR_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage WeEurDepositVault + +### WE_EUR_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 WE_EUR_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage WeEurRedemptionVault + +### WE_EUR_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 WE_EUR_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage WeEurCustomAggregatorFeed and WeEurDataFeed + +## ZeroGBtcvDepositVault + +Smart contract that handles zeroGBTCV minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## ZeroGBtcvMidasAccessControlRoles + +Base contract that stores all roles descriptors for zeroGBTCV contracts + +### ZEROG_BTCV_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 ZEROG_BTCV_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage ZeroGBtcvDepositVault + +### ZEROG_BTCV_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 ZEROG_BTCV_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage ZeroGBtcvRedemptionVault + +### ZEROG_BTCV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 ZEROG_BTCV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage ZeroGBtcvCustomAggregatorFeed and ZeroGBtcvDataFeed + +## ZeroGEthvDepositVault + +Smart contract that handles zeroGETHV minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## ZeroGEthvMidasAccessControlRoles + +Base contract that stores all roles descriptors for zeroGETHV contracts + +### ZEROG_ETHV_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 ZEROG_ETHV_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage ZeroGEthvDepositVault + +### ZEROG_ETHV_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 ZEROG_ETHV_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage ZeroGEthvRedemptionVault + +### ZEROG_ETHV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 ZEROG_ETHV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage ZeroGEthvCustomAggregatorFeed and ZeroGEthvDataFeed + +## ZeroGUsdvDepositVault + +Smart contract that handles zeroGUSDV minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## ZeroGUsdvMidasAccessControlRoles + +Base contract that stores all roles descriptors for zeroGUSDV contracts + +### ZEROG_USDV_DEPOSIT_VAULT_ADMIN_ROLE + +```solidity +bytes32 ZEROG_USDV_DEPOSIT_VAULT_ADMIN_ROLE +``` + +actor that can manage ZeroGUsdvDepositVault + +### ZEROG_USDV_REDEMPTION_VAULT_ADMIN_ROLE + +```solidity +bytes32 ZEROG_USDV_REDEMPTION_VAULT_ADMIN_ROLE +``` + +actor that can manage ZeroGUsdvRedemptionVault + +### ZEROG_USDV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 ZEROG_USDV_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +actor that can manage ZeroGUsdvCustomAggregatorFeed and ZeroGUsdvDataFeed + +## DepositVaultTest + +### _disableInitializers + +```solidity +function _disableInitializers() internal virtual +``` + +_Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. +Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized +to any version. It is recommended to use this to lock implementation contracts that are designed to be called +through proxies. + +Emits an {Initialized} event the first time it is successfully executed._ + +### tokenTransferFromToTester + +```solidity +function tokenTransferFromToTester(address token, address from, address to, uint256 amount, uint256 tokenDecimals) external +``` + +### tokenTransferToUserTester + +```solidity +function tokenTransferToUserTester(address token, address to, uint256 amount, uint256 tokenDecimals) external +``` + +### setOverrideGetTokenRate + +```solidity +function setOverrideGetTokenRate(bool val) external +``` + +### setGetTokenRateValue + +```solidity +function setGetTokenRateValue(uint256 val) external +``` + +### calcAndValidateDeposit + +```solidity +function calcAndValidateDeposit(address user, address tokenIn, uint256 amountToken, bool isInstant) external returns (struct DepositVault.CalcAndValidateDepositResult) +``` + +### convertTokenToUsdTest + +```solidity +function convertTokenToUsdTest(address tokenIn, uint256 amount) external returns (uint256 amountInUsd, uint256 rate) +``` + +### convertUsdToMTokenTest + +```solidity +function convertUsdToMTokenTest(uint256 amountUsd) external returns (uint256 amountMToken, uint256 mTokenRate) +``` + +### _getTokenRate + +```solidity +function _getTokenRate(address dataFeed, bool stable) internal view virtual returns (uint256) +``` + +_get token rate depends on data feed and stablecoin flag_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| dataFeed | address | address of dataFeed from token config | +| stable | bool | is stablecoin | + +### calculateHoldbackPartRateFromAvgTest + +```solidity +function calculateHoldbackPartRateFromAvgTest(uint256 depositedUsdAmount, uint256 depositedInstantUsdAmount, uint256 mTokenRate, uint256 avgMTokenRate) external pure returns (uint256) +``` + +## DepositVaultWithAaveTest + +### _disableInitializers + +```solidity +function _disableInitializers() internal +``` + +### _instantTransferTokensToTokensReceiver + +```solidity +function _instantTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal virtual +``` + +### _requestTransferTokensToTokensReceiver + +```solidity +function _requestTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal +``` + +### _getTokenRate + +```solidity +function _getTokenRate(address dataFeed, bool stable) internal view returns (uint256) +``` + +## DepositVaultWithMTokenTest + +### _disableInitializers + +```solidity +function _disableInitializers() internal +``` + +### _instantTransferTokensToTokensReceiver + +```solidity +function _instantTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal virtual +``` + +### _requestTransferTokensToTokensReceiver + +```solidity +function _requestTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal +``` + +### _getTokenRate + +```solidity +function _getTokenRate(address dataFeed, bool stable) internal view returns (uint256) +``` + +## DepositVaultWithMorphoTest + +### _disableInitializers + +```solidity +function _disableInitializers() internal +``` + +### _instantTransferTokensToTokensReceiver + +```solidity +function _instantTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal virtual +``` + +### _requestTransferTokensToTokensReceiver + +```solidity +function _requestTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal +``` + +### _getTokenRate + +```solidity +function _getTokenRate(address dataFeed, bool stable) internal view returns (uint256) +``` + +## DepositVaultWithUSTBTest + +### _disableInitializers + +```solidity +function _disableInitializers() internal +``` + +### _instantTransferTokensToTokensReceiver + +```solidity +function _instantTransferTokensToTokensReceiver(address tokenIn, uint256 amountToken, uint256 tokensDecimals) internal virtual +``` + +### _getTokenRate + +```solidity +function _getTokenRate(address dataFeed, bool stable) internal view returns (uint256) +``` + +## MidasAxelarVaultExecutableTester + +### constructor + +```solidity +constructor(address _depositVault, address _redemptionVault, bytes32 _paymentTokenId, bytes32 _mTokenId, address _interchainTokenService) public +``` + +### depositAndSendPublic + +```solidity +function depositAndSendPublic(bytes _depositor, uint256 _paymentTokenAmount, bytes _data) external +``` + +### depositPublic + +```solidity +function depositPublic(address _receiver, uint256 _paymentTokenAmount, uint256 _minReceiveAmount, bytes32 _referrerId) external returns (uint256 mTokenAmount) +``` + +### redeemAndSendPublic + +```solidity +function redeemAndSendPublic(bytes _redeemer, uint256 _mTokenAmount, bytes _data) external +``` + +### redeemPublic + +```solidity +function redeemPublic(address _receiver, uint256 _mTokenAmount, uint256 _minReceiveAmount) external virtual returns (uint256 paymentTokenAmount) +``` + +### balanceOfPublic + +```solidity +function balanceOfPublic(address token, address _of) external view returns (uint256) +``` + +### itsTransferPublic + +```solidity +function itsTransferPublic(string _destinationChain, bytes _destinationAddress, bytes32 _tokenId, uint256 _amount, uint256 _gasValue) external payable +``` + +### bytesToAddressPublic + +```solidity +function bytesToAddressPublic(bytes _b) external pure returns (address) +``` + +## MidasLzVaultComposerSyncTester + +### HandleComposeType + +```solidity +enum HandleComposeType { + NoOverride, + ThrowsInsufficientBalanceError, + ThrowsError +} +``` + +### handleComposeType + +```solidity +enum MidasLzVaultComposerSyncTester.HandleComposeType handleComposeType +``` + +### constructor + +```solidity +constructor(address _depositVault, address _redemptionVault, address _paymentTokenOft, address _mTokenOft) public +``` + +### setHandleComposeType + +```solidity +function setHandleComposeType(enum MidasLzVaultComposerSyncTester.HandleComposeType _handleComposeType) external +``` + +### handleCompose + +```solidity +function handleCompose(address _oftIn, bytes32 _composeFrom, bytes _composeMsg, uint256 _amount) public payable +``` + +Handles the compose operation for OFT transactions + +_This function can only be called by the contract itself (self-call restriction) + Decodes the compose message to extract SendParam and minimum message value + Routes to either deposit or redeem flow based on the input OFT token type_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _oftIn | address | The OFT token whose funds have been received in the lzReceive associated with this lzTx | +| _composeFrom | bytes32 | The bytes32 identifier of the compose sender | +| _composeMsg | bytes | The encoded message containing SendParam, minMsgValue and extraOptions | +| _amount | uint256 | The amount of tokens received in the lzReceive associated with this lzTx | + +### depositAndSendPublic + +```solidity +function depositAndSendPublic(bytes32 _depositor, uint256 _paymentTokenAmount, bytes _extraOptions, struct SendParam _sendParam, address _refundAddress) external +``` + +### depositPublic + +```solidity +function depositPublic(address _receiver, uint256 _paymentTokenAmount, uint256 _minReceiveAmount, bytes32 _referrerId) external returns (uint256 mTokenAmount) +``` + +### redeemAndSendPublic + +```solidity +function redeemAndSendPublic(bytes32 _redeemer, uint256 _mTokenAmount, bytes _extraOptions, struct SendParam _sendParam, address _refundAddress) external +``` + +### redeemPublic + +```solidity +function redeemPublic(address _receiver, uint256 _mTokenAmount, uint256 _minReceiveAmount) external virtual returns (uint256 paymentTokenAmount) +``` + +### parseExtraOptionsPublic + +```solidity +function parseExtraOptionsPublic(bytes _extraOptions) external pure returns (bytes32 referrerId) +``` + +### balanceOfPublic + +```solidity +function balanceOfPublic(address _token, address _of) external view returns (uint256) +``` + +### sendOftPublic + +```solidity +function sendOftPublic(address _oft, struct SendParam _sendParam, address _refundAddress) external payable +``` + +## RedemptionVault + +Smart contract that handles mToken redemptions + +### CalcAndValidateRedeemResult + +return data of _calcAndValidateRedeem +packed into a struct to avoid stack too deep errors + +```solidity +struct CalcAndValidateRedeemResult { + uint256 feeAmount; + uint256 amountTokenOutWithoutFee; + uint256 amountTokenOut; + uint256 tokenOutRate; + uint256 mTokenRate; + uint256 tokenOutDecimals; +} +``` + +### redeemRequests + +```solidity +mapping(uint256 => struct RequestV2) redeemRequests +``` + +mapping, requestId to request data + +### requestRedeemer + +```solidity +address requestRedeemer +``` + +address is designated for standard redemptions, allowing tokens to be pulled from this address + +### loanLp + +```solidity +address loanLp +``` + +address of loan liquidity provider + +### loanLpFeeReceiver + +```solidity +address loanLpFeeReceiver +``` + +address of loan liquidity provider fee receiver + +### loanRepaymentAddress + +```solidity +address loanRepaymentAddress +``` + +address from which payment tokens will be pulled during loan repayment + +### maxLoanApr + +```solidity +uint64 maxLoanApr +``` + +maximum loan APR value in basis points (100 = 1%) + +### preferLoanLiquidity + +```solidity +bool preferLoanLiquidity +``` + +flag to determine if the loan LP liquidity should be used first + +### loanSwapperVault + +```solidity +contract IRedemptionVault loanSwapperVault +``` + +address of loan RedemptionVault-compatible vault + +### currentLoanRequestId + +```solidity +struct Counters.Counter currentLoanRequestId +``` + +last loan request id + +### loanRequests + +```solidity +mapping(uint256 => struct LiquidityProviderLoanRequest) loanRequests +``` + +mapping, loanRequestId to loan request data + +### initialize + +```solidity +function initialize(struct CommonVaultInitParams _commonVaultInitParams, struct CommonVaultV2InitParams _commonVaultV2InitParams, struct RedemptionVaultInitParams _redemptionVaultInitParams, struct RedemptionVaultV2InitParams _redemptionVaultV2InitParams) public +``` + +upgradeable pattern contract`s initializer + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _commonVaultInitParams | struct CommonVaultInitParams | init params for common vault | +| _commonVaultV2InitParams | struct CommonVaultV2InitParams | init params for common vault v2 | +| _redemptionVaultInitParams | struct RedemptionVaultInitParams | init params for redemption vault | +| _redemptionVaultV2InitParams | struct RedemptionVaultV2InitParams | init params for redemption vault v2 | + +### initializeV2 + +```solidity +function initializeV2(struct CommonVaultV2InitParams _commonVaultV2InitParams, struct RedemptionVaultV2InitParams _redemptionVaultV2InitParams) public +``` + +v2 initializer + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _commonVaultV2InitParams | struct CommonVaultV2InitParams | | +| _redemptionVaultV2InitParams | struct RedemptionVaultV2InitParams | init params for redemption vault v2 | + +### redeemInstant + +```solidity +function redeemInstant(address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount) external +``` + +redeem mToken to tokenOut if daily limit and allowance not exceeded +Burns mToken from the user. +Transfers fee in mToken to feeReceiver +Transfers tokenOut to user. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | stable coin token address to redeem to | +| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | +| minReceiveAmount | uint256 | minimum expected amount of tokenOut to receive (decimals 18) | + +### redeemInstant + +```solidity +function redeemInstant(address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount, address recipient) external +``` + +Does the same as original `redeemInstant` but allows specifying a custom tokensReceiver address. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | stable coin token address to redeem to | +| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | +| minReceiveAmount | uint256 | minimum expected amount of tokenOut to receive (decimals 18) | +| recipient | address | address that receives tokens | + +### redeemRequest + +```solidity +function redeemRequest(address tokenOut, uint256 amountMTokenIn) external returns (uint256) +``` + +creating redeem request +Transfers amount in mToken to contract +Transfers fee in mToken to feeReceiver + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | stable coin token address to redeem to | +| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | request id | + +### redeemRequest + +```solidity +function redeemRequest(address tokenOut, uint256 amountMTokenIn, address recipient) external returns (uint256) +``` + +Does the same as original `redeemRequest` but allows specifying a custom tokensReceiver address. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | stable coin token address to redeem to | +| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | +| recipient | address | address that receives tokens | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | request id | + +### redeemRequest + +```solidity +function redeemRequest(address tokenOut, uint256 amountMTokenIn, address recipientRequest, uint256 instantShare, uint256 minReceiveAmountInstantShare, address recipientInstant) external returns (uint256) +``` + +Instantly redeems `instantShare` amount of `amountMTokenIn` and creates a request for the remaining amount. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | stable coin token address to redeem to | +| amountMTokenIn | uint256 | amount of mToken to redeem (decimals 18) | +| recipientRequest | address | address that receives tokens for the request part | +| instantShare | uint256 | % amount of `amountMTokenIn` that will be redeemed instantly | +| minReceiveAmountInstantShare | uint256 | min receive amount for the instant share | +| recipientInstant | address | address that receives tokens for the instant part | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | request id | + +### safeBulkApproveRequestAtSavedRate + +```solidity +function safeBulkApproveRequestAtSavedRate(uint256[] requestIds) external +``` + +approving requests from the `requestIds` array with the mToken rate +from the request. WONT fail even if there is not enough liquidity +to process all requests. +Does same validation as `safeApproveRequest`. +Transfers tokenOut to users +Sets request flags to Processed. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestIds | uint256[] | request ids array | + +### safeBulkApproveRequest + +```solidity +function safeBulkApproveRequest(uint256[] requestIds) external +``` + +approving requests from the `requestIds` array with the +current mToken rate. WONT fail even if there is not enough liquidity +to process all requests. +Does same validation as `safeApproveRequest`. +Transfers tokenOut to users +Sets request flags to Processed. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestIds | uint256[] | request ids array | + +### safeBulkApproveRequestAvgRate + +```solidity +function safeBulkApproveRequestAvgRate(uint256[] requestIds) external +``` + +approving requests from the `requestIds` array with the +current mToken rate as avg rate. WONT fail even if there is not enough liquidity +to process all requests. +Does same validation as `safeApproveRequestAvgRate`. +Transfers tokenOut to users +Sets request flags to Processed. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestIds | uint256[] | request ids array | + +### safeBulkApproveRequest + +```solidity +function safeBulkApproveRequest(uint256[] requestIds, uint256 newOutRate) external +``` + +approving requests from the `requestIds` array using the `newMTokenRate`. +WONT fail even if there is not enough liquidity to process all requests. +Does same validation as `safeApproveRequest`. +Transfers tokenOut to user +Sets request flags to Processed. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestIds | uint256[] | request ids array | +| newOutRate | uint256 | | + +### safeBulkApproveRequestAvgRate + +```solidity +function safeBulkApproveRequestAvgRate(uint256[] requestIds, uint256 avgMTokenRate) external +``` + +approving requests from the `requestIds` array using the `avgMTokenRate`. +WONT fail even if there is not enough liquidity to process all requests. +Does same validation as `safeApproveRequestAvgRate`. +Transfers tokenOut to user +Sets request flags to Processed. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestIds | uint256[] | request ids array | +| avgMTokenRate | uint256 | avg mToken rate inputted by vault admin | + +### approveRequest + +```solidity +function approveRequest(uint256 requestId, uint256 newMTokenRate) external +``` + +approving redeem request if not exceed tokenOut allowance +Burns amount mToken from contract +Transfers tokenOut to user +Sets flag Processed + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | +| newMTokenRate | uint256 | new mToken rate inputted by vault admin | + +### approveRequestAvgRate + +```solidity +function approveRequestAvgRate(uint256 requestId, uint256 avgMTokenRate) external +``` + +approving redeem request if not exceed tokenOut allowance +Burns amount mToken from contract +Transfers tokenOut to user +Sets flag Processed + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | +| avgMTokenRate | uint256 | avg mToken rate inputted by vault admin | + +### safeApproveRequest + +```solidity +function safeApproveRequest(uint256 requestId, uint256 newMTokenRate) external +``` + +approving request if inputted token rate fit price diviation percent +Burns amount mToken from contract +Transfers tokenOut to user +Sets flag Processed + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | +| newMTokenRate | uint256 | new mToken rate inputted by vault admin | + +### safeApproveRequestAvgRate + +```solidity +function safeApproveRequestAvgRate(uint256 requestId, uint256 avgMTokenRate) external +``` + +approving request if inputted token rate fit price diviation percent +Burns amount mToken from contract +Transfers tokenOut to user +Sets flag Processed + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | +| avgMTokenRate | uint256 | avg mToken rate inputted by vault admin | + +### rejectRequest + +```solidity +function rejectRequest(uint256 requestId) external +``` + +rejecting request +Sets request flag to Canceled. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | + +### bulkRepayLpLoanRequest + +```solidity +function bulkRepayLpLoanRequest(uint256[] requestIds, uint64 loanApr) external +``` + +repaying loan requests from the `requestIds` array +Transfers tokenOut to loan repayment address +Transfers fee in tokenOut to loan lp fee receiver +Sets request flags to Processed. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestIds | uint256[] | request ids array | +| loanApr | uint64 | loan APR. Overrides calculated loan fee in case if accrued interest is greater than the calculated loan fee. | + +### cancelLpLoanRequest + +```solidity +function cancelLpLoanRequest(uint256 requestId) external +``` + +canceling loan request +Sets request flags to Canceled. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | + +### setRequestRedeemer + +```solidity +function setRequestRedeemer(address redeemer) external +``` + +set address which is designated for standard redemptions, allowing tokens to be pulled from this address + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| redeemer | address | new address of request redeemer | + +### setLoanLp + +```solidity +function setLoanLp(address newLoanLp) external +``` + +set address of loan liquidity provider + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newLoanLp | address | new address of loan liquidity provider | + +### setLoanLpFeeReceiver + +```solidity +function setLoanLpFeeReceiver(address newLoanLpFeeReceiver) external +``` + +set address of loan liquidity provider fee receiver + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newLoanLpFeeReceiver | address | new address of loan liquidity provider fee receiver | + +### setLoanRepaymentAddress + +```solidity +function setLoanRepaymentAddress(address newLoanRepaymentAddress) external +``` + +set address of loan repayment address + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newLoanRepaymentAddress | address | new address of loan repayment address | + +### setLoanSwapperVault + +```solidity +function setLoanSwapperVault(address newLoanSwapperVault) external +``` + +set address of loan swapper vault + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newLoanSwapperVault | address | new address of loan swapper vault | + +### setMaxLoanApr + +```solidity +function setMaxLoanApr(uint64 newMaxLoanApr) external +``` + +set maximum loan APR value in basis points (100 = 1%) + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newMaxLoanApr | uint64 | new maximum loan APR value in basis points (100 = 1%) | + +### setPreferLoanLiquidity + +```solidity +function setPreferLoanLiquidity(bool newLoanLpFirst) external +``` + +set flag to determine if the loan LP liquidity should be used first + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newLoanLpFirst | bool | new flag to determine if the loan LP liquidity should be used first | + +### vaultRole + +```solidity +function vaultRole() public pure virtual returns (bytes32) +``` + +AC role of vault administrator + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role bytes32 role | + +### _safeBulkApproveRequest + +```solidity +function _safeBulkApproveRequest(uint256[] requestIds, uint256 newOutRate, bool isAvgRate) internal +``` + +_internal function to approve requests_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestIds | uint256[] | request ids | +| newOutRate | uint256 | new out rate | +| isAvgRate | bool | if true, newOutRate is avg rate | + +### _approveRequest + +```solidity +function _approveRequest(uint256 requestId, uint256 newMTokenRate, bool isSafe, bool safeValidateLiquidity, bool isAvgRate) internal returns (bool) +``` + +_validates approve +burns amount from contract +transfer tokenOut to user +sets flag Processed_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | +| newMTokenRate | uint256 | new mToken rate | +| isSafe | bool | new mToken rate | +| safeValidateLiquidity | bool | if true, checks if there is enough liquidity and if its not sufficient, function wont fail | +| isAvgRate | bool | if true, calculates holdback part rate from avg rate | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | success true if success, false only in case if safeValidateLiquidity == true and there is not enough liquidity | + +### _validateRequest + +```solidity +function _validateRequest(address validateAddress, enum RequestStatus status) internal pure +``` + +validates request +if exist +if not processed + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| validateAddress | address | address to check if not zero | +| status | enum RequestStatus | request status | + +### _redeemInstant + +```solidity +function _redeemInstant(address tokenOut, uint256 amountMTokenIn, uint256 minReceiveAmount, address) internal virtual returns (struct RedemptionVault.CalcAndValidateRedeemResult calcResult) +``` + +_internal redeem instant logic_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | tokenOut address | +| amountMTokenIn | uint256 | amount of mToken (decimals 18) | +| minReceiveAmount | uint256 | min amount of tokenOut to receive (decimals 18) | +| | address | | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| calcResult | struct RedemptionVault.CalcAndValidateRedeemResult | calculated redeem result | + +### _sendTokensFromLiquidity + +```solidity +function _sendTokensFromLiquidity(address tokenOut, address recipient, struct RedemptionVault.CalcAndValidateRedeemResult calcResult) internal +``` + +_Sends tokens from liquidity to the recipient_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | tokenOut address | +| recipient | address | recipient address | +| calcResult | struct RedemptionVault.CalcAndValidateRedeemResult | calculated redeem result | + +### _useVaultLiquidity + +```solidity +function _useVaultLiquidity(address, uint256, uint256, uint256, uint256) internal virtual returns (uint256) +``` + +_Check if contract has enough tokenOut balance for redeem, +if not, obtains liquidity trough the custom strategies. +In default implementation it does nothing._ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | obtainedLiquidityBase18 amount of tokenOut obtained | + +### _useLoanLpLiquidity + +```solidity +function _useLoanLpLiquidity(address tokenOut, uint256 missingAmountBase18, uint256 totalAmount, uint256 tokenOutRate, uint256 totalFee, uint256 tokenOutDecimals) internal returns (uint256, uint256) +``` + +_Check if contract has enough tokenOut balance for redeem; +if not, redeem the missing amount via loan LP liquidity_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | tokenOut address | +| missingAmountBase18 | uint256 | amount of tokenOut needed in base 18 | +| totalAmount | uint256 | total amount of tokenOut needed in base 18 | +| tokenOutRate | uint256 | tokenOut rate | +| totalFee | uint256 | total fee of tokenOut | +| tokenOutDecimals | uint256 | decimals of tokenOut | + +### _redeemRequest + +```solidity +function _redeemRequest(address tokenOut, uint256 amountMTokenIn, address recipient, uint256 amountMTokenInstant) internal returns (uint256 requestId) +``` + +internal redeem request logic + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | tokenOut address | +| amountMTokenIn | uint256 | amount of mToken (decimals 18) | +| recipient | address | recipient address | +| amountMTokenInstant | uint256 | amount of mToken that was redeemed instantly | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| requestId | uint256 | request id | + +### _convertUsdToToken + +```solidity +function _convertUsdToToken(uint256 amountUsd, address tokenOut, uint256 overrideTokenRate) internal view returns (uint256 amountToken, uint256 tokenRate) +``` + +_calculates tokenOut amount from USD amount_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| amountUsd | uint256 | amount of USD (decimals 18) | +| tokenOut | address | tokenOut address | +| overrideTokenRate | uint256 | override token rate if not zero | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| amountToken | uint256 | converted USD to tokenOut | +| tokenRate | uint256 | conversion rate | + +### _convertMTokenToUsd + +```solidity +function _convertMTokenToUsd(uint256 amountMToken, uint256 overrideTokenRate) internal view returns (uint256 amountUsd, uint256 mTokenRate) +``` + +_calculates USD amount from mToken amount_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| amountMToken | uint256 | amount of mToken (decimals 18) | +| overrideTokenRate | uint256 | override mToken rate if not zero | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| amountUsd | uint256 | converted amount to USD | +| mTokenRate | uint256 | conversion rate | + +### _calcAndValidateRedeem + +```solidity +function _calcAndValidateRedeem(address user, address tokenOut, uint256 amountMTokenIn, uint256 overrideMTokenRate, uint256 overrideTokenOutRate, bool shouldOverrideFeePercent, uint256 overrideFeePercent, bool isInstant) internal view virtual returns (struct RedemptionVault.CalcAndValidateRedeemResult result) +``` + +_validate redeem and calculate fee_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| user | address | user address | +| tokenOut | address | tokenOut address | +| amountMTokenIn | uint256 | mToken amount (decimals 18) | +| overrideMTokenRate | uint256 | override mToken rate if not zero | +| overrideTokenOutRate | uint256 | override token rate if not zero | +| shouldOverrideFeePercent | bool | should override fee percent if true | +| overrideFeePercent | uint256 | override fee percent if shouldOverrideFeePercent is true | +| isInstant | bool | is instant operation | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| result | struct RedemptionVault.CalcAndValidateRedeemResult | calc result | + +### _convertMTokenToTokenOut + +```solidity +function _convertMTokenToTokenOut(uint256 amountMTokenIn, uint256 overrideMTokenRate, address tokenOut, uint256 overrideTokenOutRate) internal view returns (uint256, uint256, uint256) +``` + +_converts mToken to tokenOut amount_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| amountMTokenIn | uint256 | amount of mToken | +| overrideMTokenRate | uint256 | override mToken rate if not zero | +| tokenOut | address | tokenOut address | +| overrideTokenOutRate | uint256 | override token rate if not zero | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | amountTokenOut amount of tokenOut | +| [1] | uint256 | mTokenRate conversion rate | +| [2] | uint256 | tokenOutRate conversion rate | + +### _validateMTokenAmount + +```solidity +function _validateMTokenAmount(address user, uint256 amountMTokenIn) internal view +``` + +_validates mToken amount for different constraints_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| user | address | user address | +| amountMTokenIn | uint256 | amount of mToken | + +### _validateLiquidity + +```solidity +function _validateLiquidity(address token, uint256 requiredLiquidity, uint256 tokenDecimals) internal view returns (bool) +``` + +### _calculateHoldbackPartRateFromAvg + +```solidity +function _calculateHoldbackPartRateFromAvg(struct RequestV2 request, uint256 avgMTokenRate) internal pure returns (uint256) +``` + +_calculates holdback part rate from avg rate_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| request | struct RequestV2 | request | +| avgMTokenRate | uint256 | avg mToken rate | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | holdback part rate | + +## RedemptionVaultWithAave + +Smart contract that handles redemptions using Aave V3 Pool withdrawals + +_When the vault has insufficient payment token balance, it withdraws from +an Aave V3 Pool by burning its aTokens to obtain the underlying asset._ + +### aavePools + +```solidity +mapping(address => contract IAaveV3Pool) aavePools +``` + +mapping payment token to Aave V3 Pool + +### SetAavePool + +```solidity +event SetAavePool(address caller, address token, address pool) +``` + +Emitted when an Aave V3 Pool is configured for a payment token + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | address of the caller | +| token | address | payment token address | +| pool | address | Aave V3 Pool address | + +### RemoveAavePool + +```solidity +event RemoveAavePool(address caller, address token) +``` + +Emitted when an Aave V3 Pool is removed for a payment token + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | address of the caller | +| token | address | payment token address | + +### setAavePool + +```solidity +function setAavePool(address _token, address _aavePool) external +``` + +Sets the Aave V3 Pool for a specific payment token + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _token | address | payment token address | +| _aavePool | address | Aave V3 Pool address for this token | + +### removeAavePool + +```solidity +function removeAavePool(address _token) external +``` + +Removes the Aave V3 Pool for a specific payment token + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _token | address | payment token address | + +### _useVaultLiquidity + +```solidity +function _useVaultLiquidity(address tokenOut, uint256 missingAmountBase18, uint256, uint256, uint256 tokenOutDecimals) internal virtual returns (uint256) +``` + +Check if contract has enough tokenOut balance for redeem; +if not, withdraw the missing amount from the Aave V3 Pool + +_The Aave Pool burns the vault's aTokens and transfers the underlying +asset directly to this contract. No approval is needed because the Pool +burns aTokens from msg.sender (this contract) internally._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | tokenOut address | +| missingAmountBase18 | uint256 | amount of tokenOut needed in base 18 | +| | uint256 | | +| | uint256 | | +| tokenOutDecimals | uint256 | decimals of tokenOut | + +## RedemptionVaultWithMToken + +Smart contract that handles redemptions using mToken RedemptionVault withdrawals + +_Storage layout is preserved for safe upgrades from RedemptionVaultWithSwapper_ + +### redemptionVault + +```solidity +contract IRedemptionVault redemptionVault +``` + +### liquidityProvider_deprecated + +```solidity +address liquidityProvider_deprecated +``` + +### SetRedemptionVault + +```solidity +event SetRedemptionVault(address caller, address newVault) +``` + +Emitted when the redemption vault address is updated + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | address of the caller | +| newVault | address | new redemption vault address | + +### initialize + +```solidity +function initialize(struct CommonVaultInitParams _commonVaultInitParams, struct CommonVaultV2InitParams _commonVaultV2InitParams, struct RedemptionVaultInitParams _redemptionInitParams, struct RedemptionVaultV2InitParams _redemptionVaultV2InitParams, address _redemptionVault) external +``` + +upgradeable pattern contract`s initializer + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _commonVaultInitParams | struct CommonVaultInitParams | init params for common vault | +| _commonVaultV2InitParams | struct CommonVaultV2InitParams | init params for common vault v2 | +| _redemptionInitParams | struct RedemptionVaultInitParams | init params for redemption vault state values | +| _redemptionVaultV2InitParams | struct RedemptionVaultV2InitParams | init params for redemption vault v2 | +| _redemptionVault | address | address of the mTokenA RedemptionVault | + +### setRedemptionVault + +```solidity +function setRedemptionVault(address _redemptionVault) external +``` + +Sets the mTokenA RedemptionVault address + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _redemptionVault | address | new RedemptionVault address | + +### _useVaultLiquidity + +```solidity +function _useVaultLiquidity(address tokenOut, uint256 missingAmountBase18, uint256 tokenOutRate, uint256, uint256) internal virtual returns (uint256) +``` + +Check if contract has enough tokenOut balance for redeem; +if not, redeem the missing amount via mToken RedemptionVault + +_The other vault burns this contract's mToken and transfers the +underlying asset to this contract_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | tokenOut address | +| missingAmountBase18 | uint256 | amount of tokenOut needed in base 18 | +| tokenOutRate | uint256 | tokenOut rate | +| | uint256 | | +| | uint256 | | + +## RedemptionVaultWithMorpho + +Smart contract that handles redemptions using Morpho Vault withdrawals + +_When the vault has insufficient payment token balance, it withdraws from +a Morpho Vault (ERC-4626) by burning its vault shares to obtain the underlying asset. +Works with both Morpho Vaults V1 (MetaMorpho) and V2._ + +### morphoVaults + +```solidity +mapping(address => contract IMorphoVault) morphoVaults +``` + +mapping payment token to Morpho Vault + +### SetMorphoVault + +```solidity +event SetMorphoVault(address caller, address token, address vault) +``` + +Emitted when a Morpho Vault is configured for a payment token + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | address of the caller | +| token | address | payment token address | +| vault | address | Morpho Vault address | + +### RemoveMorphoVault + +```solidity +event RemoveMorphoVault(address caller, address token) +``` + +Emitted when a Morpho Vault is removed for a payment token + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | address of the caller | +| token | address | payment token address | + +### setMorphoVault + +```solidity +function setMorphoVault(address _token, address _morphoVault) external +``` + +Sets the Morpho Vault for a specific payment token + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _token | address | payment token address | +| _morphoVault | address | Morpho Vault (ERC-4626) address for this token | + +### removeMorphoVault + +```solidity +function removeMorphoVault(address _token) external +``` + +Removes the Morpho Vault for a specific payment token + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _token | address | payment token address | + +### _useVaultLiquidity + +```solidity +function _useVaultLiquidity(address tokenOut, uint256 missingAmountBase18, uint256, uint256, uint256 tokenOutDecimals) internal virtual returns (uint256) +``` + +Check if contract has enough tokenOut balance for redeem; +if not, withdraw the missing amount from the Morpho Vault + +_The Morpho Vault burns the vault's shares and transfers the underlying +asset directly to this contract. No approval is needed because the vault +burns shares from msg.sender (this contract) when msg.sender == owner._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | tokenOut address | +| missingAmountBase18 | uint256 | amount of tokenOut needed in base 18 | +| | uint256 | | +| | uint256 | | +| tokenOutDecimals | uint256 | decimals of tokenOut | + +## RedemptionVaultWithSwapper + +Legacy swapper contract that is keeped for layout compatibility +with already deployed contracts. + +Legacy description: +Smart contract that handles mToken redemption. +In case of insufficient liquidity it uses a RV from a different +Midas product to fulfill instant redemption. + +## RedemptionVaultWithUSTB + +Smart contract that handles redemptions using USTB + +### ustbRedemption + +```solidity +contract IUSTBRedemption ustbRedemption +``` + +USTB redemption contract address + +_Used to handle USTB redemptions when vault has insufficient USDC_ + +### initialize + +```solidity +function initialize(struct CommonVaultInitParams _commonVaultInitParams, struct CommonVaultV2InitParams _commonVaultV2InitParams, struct RedemptionVaultInitParams _redemptionInitParams, struct RedemptionVaultV2InitParams _redemptionVaultV2InitParams, address _ustbRedemption) external +``` + +upgradeable pattern contract`s initializer + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _commonVaultInitParams | struct CommonVaultInitParams | init params for common vault | +| _commonVaultV2InitParams | struct CommonVaultV2InitParams | init params for common vault v2 | +| _redemptionInitParams | struct RedemptionVaultInitParams | init params for redemption vault state values | +| _redemptionVaultV2InitParams | struct RedemptionVaultV2InitParams | init params for redemption vault v2 | +| _ustbRedemption | address | USTB redemption contract address | + +### _useVaultLiquidity + +```solidity +function _useVaultLiquidity(address tokenOut, uint256 missingAmountBase18, uint256, uint256 currentTokenOutBalanceBase18, uint256 tokenOutDecimals) internal virtual returns (uint256) +``` + +Check if contract has enough USDC balance for redeem +if not, trigger USTB redemption flow to redeem exactly the missing amount + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenOut | address | tokenOut address | +| missingAmountBase18 | uint256 | amount of tokenOut needed in base 18 | +| | uint256 | | +| currentTokenOutBalanceBase18 | uint256 | current balance of tokenOut in the vault in base 18 | +| tokenOutDecimals | uint256 | decimals of tokenOut | + +## IUSTBRedemption + +### SUPERSTATE_TOKEN + +```solidity +function SUPERSTATE_TOKEN() external view returns (address) +``` + +### USDC + +```solidity +function USDC() external view returns (address) +``` + +### owner + +```solidity +function owner() external view returns (address) +``` + +### redeem + +```solidity +function redeem(uint256 superstateTokenInAmount) external +``` + +### setRedemptionFee + +```solidity +function setRedemptionFee(uint256 _newFee) external +``` + +### calculateFee + +```solidity +function calculateFee(uint256 amount) external view returns (uint256) +``` + +### calculateUstbIn + +```solidity +function calculateUstbIn(uint256 usdcOutAmount) external view returns (uint256 ustbInAmount, uint256 usdPerUstbChainlinkRaw) +``` + +## JivRedemptionVaultWithSwapper + +Smart contract that handles JIV redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## AcreMBtc1RedemptionVaultWithSwapper + +Smart contract that handles acremBTC1 redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## CUsdoRedemptionVaultWithSwapper + +Smart contract that handles cUSDO redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## DnEthRedemptionVaultWithSwapper + +Smart contract that handles dnETH redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## DnFartRedemptionVaultWithSwapper + +Smart contract that handles dnFART redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## DnHypeRedemptionVaultWithSwapper + +Smart contract that handles dnHYPE redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## DnPumpRedemptionVaultWithSwapper + +Smart contract that handles dnPUMP redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## DnTestRedemptionVaultWithSwapper + +Smart contract that handles dnTEST redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## EUsdRedemptionVault + +Smart contract that handles eUSD redeeming + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +### greenlistedRole + +```solidity +function greenlistedRole() public pure returns (bytes32) +``` + +AC role of a greenlist + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role bytes32 role | + +## HBUsdcRedemptionVaultWithSwapper + +Smart contract that handles hbUSDC redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## HBUsdtRedemptionVaultWithSwapper + +Smart contract that handles hbUSDT redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## HBXautRedemptionVaultWithSwapper + +Smart contract that handles hbXAUt redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## HypeBtcRedemptionVaultWithSwapper + +Smart contract that handles hypeBTC redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## HypeEthRedemptionVaultWithSwapper + +Smart contract that handles hypeETH redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## HypeUsdRedemptionVaultWithSwapper + +Smart contract that handles hypeUSD redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## KitBtcRedemptionVaultWithSwapper + +Smart contract that handles kitBTC redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## KitHypeRedemptionVaultWithSwapper + +Smart contract that handles kitHYPE redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## KitUsdRedemptionVaultWithSwapper + +Smart contract that handles kitUSD redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## KmiUsdRedemptionVaultWithSwapper + +Smart contract that handles kmiUSD redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## LiquidHypeRedemptionVaultWithSwapper + +Smart contract that handles liquidHYPE redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## LiquidReserveRedemptionVaultWithSwapper + +Smart contract that handles liquidRESERVE redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## LstHypeRedemptionVaultWithSwapper + +Smart contract that handles lstHYPE redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MApolloRedemptionVaultWithSwapper + +Smart contract that handles mAPOLLO redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MBasisRedemptionVault + +Smart contract that handles mBASIS minting + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MBasisRedemptionVaultWithSwapper + +Smart contract that handles mBASIS redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MBtcRedemptionVault + +Smart contract that handles mBTC redemption + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## TACmBtcRedemptionVault + +Smart contract that handles TACmBTC redemption + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MEdgeRedemptionVaultWithSwapper + +Smart contract that handles mEDGE redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## TACmEdgeRedemptionVault + +Smart contract that handles TACmEDGE redemption + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MEvUsdRedemptionVaultWithSwapper + +Smart contract that handles mEVUSD redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MFarmRedemptionVaultWithSwapper + +Smart contract that handles mFARM redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MFOneRedemptionVaultWithMToken + +Smart contract that handles mF-ONE redemptions using mToken +liquid strategy. Upgrade-compatible replacement for +MFOneRedemptionVaultWithSwapper. + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MFOneRedemptionVaultWithSwapper + +Smart contract that handles mF-ONE redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MHyperRedemptionVaultWithSwapper + +Smart contract that handles mHYPER redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MHyperBtcRedemptionVaultWithSwapper + +Smart contract that handles mHyperBTC redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MHyperEthRedemptionVaultWithSwapper + +Smart contract that handles mHyperETH redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MKRalphaRedemptionVaultWithSwapper + +Smart contract that handles mKRalpha redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MLiquidityRedemptionVault + +Smart contract that handles mLIQUIDITY redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MM1UsdRedemptionVaultWithSwapper + +Smart contract that handles mM1USD redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MMevRedemptionVaultWithSwapper + +Smart contract that handles mMEV redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## TACmMevRedemptionVault + +Smart contract that handles TACmMEV redemption + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MPortofinoRedemptionVaultWithSwapper + +Smart contract that handles mPortofino redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MRe7RedemptionVaultWithSwapper + +Smart contract that handles mRE7 redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MRe7BtcRedemptionVaultWithSwapper + +Smart contract that handles mRE7BTC redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MRe7SolRedemptionVault + +Smart contract that handles mRE7SOL redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MRoxRedemptionVaultWithSwapper + +Smart contract that handles mROX redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MSlRedemptionVaultWithMToken + +Smart contract that handles mSL redemptions using mToken +liquid strategy. Upgrade-compatible replacement for +MSlRedemptionVaultWithSwapper. + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MSlRedemptionVaultWithSwapper + +Smart contract that handles mSL redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MTuRedemptionVaultWithSwapper + +Smart contract that handles mTU redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MWildUsdRedemptionVaultWithSwapper + +Smart contract that handles mWildUSD redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MXrpRedemptionVaultWithSwapper + +Smart contract that handles mXRP redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MevBtcRedemptionVaultWithSwapper + +Smart contract that handles mevBTC redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MSyrupUsdRedemptionVaultWithSwapper + +Smart contract that handles msyrupUSD redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## MSyrupUsdpRedemptionVaultWithSwapper + +Smart contract that handles msyrupUSDp redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## ObeatUsdRedemptionVaultWithSwapper + +Smart contract that handles obeatUSD redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## PlUsdRedemptionVaultWithSwapper + +Smart contract that handles plUSD redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## SLInjRedemptionVaultWithSwapper + +Smart contract that handles sLINJ redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## SplUsdRedemptionVaultWithSwapper + +Smart contract that handles splUSD redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## TBtcRedemptionVaultWithSwapper + +Smart contract that handles tBTC redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## TEthRedemptionVaultWithSwapper + +Smart contract that handles tETH redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## TUsdeRedemptionVaultWithSwapper + +Smart contract that handles tUSDe redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## TacTonRedemptionVaultWithSwapper + +Smart contract that handles tacTON redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## WNlpRedemptionVaultWithSwapper + +Smart contract that handles wNLP redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## WVLPRedemptionVaultWithSwapper + +Smart contract that handles wVLP redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## WeEurRedemptionVaultWithSwapper + +Smart contract that handles weEUR redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## ZeroGBtcvRedemptionVaultWithSwapper + +Smart contract that handles zeroGBTCV redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## ZeroGEthvRedemptionVaultWithSwapper + +Smart contract that handles zeroGETHV redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## ZeroGUsdvRedemptionVaultWithSwapper + +Smart contract that handles zeroGUSDV redemptions + +### vaultRole + +```solidity +function vaultRole() public pure returns (bytes32) +``` + +## RedemptionVaultTest + +### _disableInitializers + +```solidity +function _disableInitializers() internal virtual +``` + +_Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. +Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized +to any version. It is recommended to use this to lock implementation contracts that are designed to be called +through proxies. + +Emits an {Initialized} event the first time it is successfully executed._ + +### setOverrideGetTokenRate + +```solidity +function setOverrideGetTokenRate(bool val) external +``` + +### setGetTokenRateValue + +```solidity +function setGetTokenRateValue(uint256 val) external +``` + +### calcAndValidateRedeemTest + +```solidity +function calcAndValidateRedeemTest(address user, address tokenOut, uint256 amountMTokenIn, uint256 overrideMTokenRate, uint256 overrideTokenOutRate, bool shouldOverrideFeePercent, uint256 overrideFeePercent, bool isInstant) external returns (struct RedemptionVault.CalcAndValidateRedeemResult calcResult) +``` + +### calculateHoldbackPartRateFromAvgTest + +```solidity +function calculateHoldbackPartRateFromAvgTest(uint256 amountMToken, uint256 amountMTokenInstant, uint256 mTokenRate, uint256 avgMTokenRate) external pure returns (uint256) +``` + +### convertUsdToTokenTest + +```solidity +function convertUsdToTokenTest(uint256 amountUsd, address tokenOut, uint256 overrideTokenOutRate) external returns (uint256 amountToken, uint256 tokenRate) +``` + +### convertMTokenToUsdTest + +```solidity +function convertMTokenToUsdTest(uint256 amountMToken, uint256 overrideMTokenRate) external returns (uint256 amountUsd, uint256 mTokenRate) +``` + +### _getTokenRate + +```solidity +function _getTokenRate(address dataFeed, bool stable) internal view virtual returns (uint256) +``` + +_get token rate depends on data feed and stablecoin flag_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| dataFeed | address | address of dataFeed from token config | +| stable | bool | is stablecoin | + +## RedemptionVaultWithAaveTest + +### _disableInitializers + +```solidity +function _disableInitializers() internal virtual +``` + +### checkAndRedeemAave + +```solidity +function checkAndRedeemAave(address token, uint256 amount) external returns (uint256) +``` + +### _useVaultLiquidity + +```solidity +function _useVaultLiquidity(address tokenOut, uint256 amountTokenOutBase18, uint256 tokenOutRate, uint256 currentTokenOutBalanceBase18, uint256 tokenOutDecimals) internal returns (uint256) +``` + +### _getTokenRate + +```solidity +function _getTokenRate(address dataFeed, bool stable) internal view returns (uint256) +``` + +## RedemptionVaultWithMTokenTest + +### _disableInitializers + +```solidity +function _disableInitializers() internal virtual +``` + +### checkAndRedeemMToken + +```solidity +function checkAndRedeemMToken(address token, uint256 amount, uint256 rate) external returns (uint256) +``` + +### _useVaultLiquidity + +```solidity +function _useVaultLiquidity(address token, uint256 amountTokenOutBase18, uint256 tokenOutRate, uint256 currentTokenOutBalanceBase18, uint256 tokenOutDecimals) internal returns (uint256) +``` + +### _getTokenRate + +```solidity +function _getTokenRate(address dataFeed, bool stable) internal view returns (uint256) +``` + +## RedemptionVaultWithMorphoTest + +### _disableInitializers + +```solidity +function _disableInitializers() internal virtual +``` + +### checkAndRedeemMorpho + +```solidity +function checkAndRedeemMorpho(address token, uint256 amount) external returns (uint256) +``` + +### _useVaultLiquidity + +```solidity +function _useVaultLiquidity(address token, uint256 amountTokenOutBase18, uint256 tokenOutRate, uint256 currentTokenOutBalanceBase18, uint256 tokenOutDecimals) internal returns (uint256) +``` + +### _getTokenRate + +```solidity +function _getTokenRate(address dataFeed, bool stable) internal view returns (uint256) +``` + +## RedemptionVaultWithSwapperTest + +### _disableInitializers + +```solidity +function _disableInitializers() internal +``` + +_Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. +Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized +to any version. It is recommended to use this to lock implementation contracts that are designed to be called +through proxies. + +Emits an {Initialized} event the first time it is successfully executed._ + +## RedemptionVaultWithUSTBTest + +### _disableInitializers + +```solidity +function _disableInitializers() internal virtual +``` + +### checkAndRedeemUSTB + +```solidity +function checkAndRedeemUSTB(address token, uint256 amount) external returns (uint256) +``` + +### _useVaultLiquidity + +```solidity +function _useVaultLiquidity(address tokenOut, uint256 amountTokenOutBase18, uint256 tokenOutRate, uint256 currentTokenOutBalanceBase18, uint256 tokenOutDecimals) internal returns (uint256) +``` + +### _getTokenRate + +```solidity +function _getTokenRate(address dataFeed, bool stable) internal view returns (uint256) +``` + +## IRedemptionVaultWithSwapper + +### SetLiquidityProvider + +```solidity +event SetLiquidityProvider(address caller, address provider) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | caller address (msg.sender) | +| provider | address | new LP address | + +### SetSwapperVault + +```solidity +event SetSwapperVault(address caller, address vault) +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| caller | address | caller address (msg.sender) | +| vault | address | new underlying vault for swapper | + +### setLiquidityProvider + +```solidity +function setLiquidityProvider(address provider) external +``` + +sets new liquidity provider address + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| provider | address | new liquidity provider address | + +### setSwapperVault + +```solidity +function setSwapperVault(address vault) external +``` + +sets new underlying vault for swapper + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| vault | address | new underlying vault for swapper | + +## MidasTimelockController + +Default TimelockController but with getters for proposers and executors + +### constructor + +```solidity +constructor(uint256 minDelay, address[] proposers, address[] executors) public +``` + +### getInitialProposers + +```solidity +function getInitialProposers() external view returns (address[]) +``` + +Get all the initial proposers + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | address[] | initial proposers addresses | + +### getInitialExecutors + +```solidity +function getInitialExecutors() external view returns (address[]) +``` + +Get all the initial executors + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | address[] | initial executors addresses | + +## CompositeDataFeed + +A data feed contract that derives its price by computing the ratio +of two underlying data feeds (numerator ÷ denominator). + +_Designed for cases where a synthetic or relative price is needed, +such as deriving cbBTC/BTC from cbBTC/USD and BTC/USD feeds._ + +### numeratorFeed + +```solidity +contract IDataFeed numeratorFeed +``` + +price feed used as the numerator in the ratio calculation. + +_typically represents the asset of interest (e.g., cbBTC/USD)._ + +### denominatorFeed + +```solidity +contract IDataFeed denominatorFeed +``` + +price feed used as the denominator in the ratio calculation. + +_typically represents the reference asset (e.g., BTC/USD)._ + +### minExpectedAnswer + +```solidity +uint256 minExpectedAnswer +``` + +_minimal answer expected to receive from getDataInBase18_ + +### maxExpectedAnswer + +```solidity +uint256 maxExpectedAnswer +``` + +_maximal answer expected to receive from getDataInBase18_ + +### initialize + +```solidity +function initialize(address _ac, address _numeratorFeed, address _denominatorFeed, uint256 _minExpectedAnswer, uint256 _maxExpectedAnswer) external +``` + +upgradeable pattern contract`s initializer + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _ac | address | MidasAccessControl contract address | +| _numeratorFeed | address | numerator feed address | +| _denominatorFeed | address | denominator feed address | +| _minExpectedAnswer | uint256 | min. expected answer value from data feed | +| _maxExpectedAnswer | uint256 | max. expected answer value from data feed | + +### changeNumeratorFeed + +```solidity +function changeNumeratorFeed(address _numeratorFeed) external +``` + +updates `numeratorFeed` address + +_can only be called by the feed admin_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _numeratorFeed | address | new numerator feed address | + +### changeDenominatorFeed + +```solidity +function changeDenominatorFeed(address _denominatorFeed) external +``` + +updates `denominatorFeed` address + +_can only be called by the feed admin_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _denominatorFeed | address | new denominator feed address | + +### setMinExpectedAnswer + +```solidity +function setMinExpectedAnswer(uint256 _minExpectedAnswer) external +``` + +_updates `minExpectedAnswer` value_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _minExpectedAnswer | uint256 | min value | + +### setMaxExpectedAnswer + +```solidity +function setMaxExpectedAnswer(uint256 _maxExpectedAnswer) external +``` + +_updates `maxExpectedAnswer` value_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _maxExpectedAnswer | uint256 | max value | + +### getDataInBase18 + +```solidity +function getDataInBase18() external view returns (uint256 answer) +``` + +_fetches answer from numerator and denominator feeds +and returns calculated answer (numerator / denominator)_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| answer | uint256 | calculated answer in base18 | + +### _computeCompositePrice + +```solidity +function _computeCompositePrice(uint256 numerator, uint256 denominator) internal pure virtual returns (uint256 answer) +``` + +_computes the composite price by dividing numerator by denominator_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| numerator | uint256 | numerator value from the first feed | +| denominator | uint256 | denominator value from the second feed | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| answer | uint256 | computed composite price in base18 | + +### feedAdminRole + +```solidity +function feedAdminRole() public pure virtual returns (bytes32) +``` + +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## CompositeDataFeedMultiply + +A data feed contract that derives its price by computing the product +of two underlying data feeds (numerator × denominator). + +_Inherits from CompositeDataFeed and overrides only the calculation logic +to multiply instead of divide. Designed for cases where a synthetic or combined +price is needed, such as deriving mXRP/USD from mXRP/XRP and XRP/USD feeds._ + +### _computeCompositePrice + +```solidity +function _computeCompositePrice(uint256 firstFeedValue, uint256 secondFeedValue) internal pure returns (uint256 answer) +``` + +_computes the composite price by multiplying the two feed values_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| firstFeedValue | uint256 | value from the first feed | +| secondFeedValue | uint256 | value from the second feed | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| answer | uint256 | computed composite price in base18 | + +## CustomAggregatorV3CompatibleFeed + +AggregatorV3 compatible feed, where price is submitted manually by feed admins + +### RoundData + +```solidity +struct RoundData { + uint80 roundId; + int256 answer; + uint256 startedAt; + uint256 updatedAt; + uint80 answeredInRound; +} +``` + +### description + +```solidity +string description +``` + +feed description + +### latestRound + +```solidity +uint80 latestRound +``` + +last round id + +### maxAnswerDeviation + +```solidity +uint256 maxAnswerDeviation +``` + +max deviation from lattest price in % + +_10 ** decimals() is a percentage precision_ + +### minAnswer + +```solidity +int192 minAnswer +``` + +minimal possible answer that feed can return + +### maxAnswer + +```solidity +int192 maxAnswer +``` + +maximal possible answer that feed can return + +### AnswerUpdated + +```solidity +event AnswerUpdated(int256 data, uint256 roundId, uint256 timestamp) +``` + +### onlyAggregatorAdmin + +```solidity +modifier onlyAggregatorAdmin() +``` + +_checks that msg.sender do have a feedAdminRole() role_ + +### initialize + +```solidity +function initialize(address _accessControl, int192 _minAnswer, int192 _maxAnswer, uint256 _maxAnswerDeviation, string _description) public virtual +``` + +upgradeable pattern contract`s initializer + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _accessControl | address | address of MidasAccessControll contract | +| _minAnswer | int192 | init value for `minAnswer`. Should be < `_maxAnswer` | +| _maxAnswer | int192 | init value for `maxAnswer`. Should be > `_minAnswer` | +| _maxAnswerDeviation | uint256 | init value for `maxAnswerDeviation` | +| _description | string | init value for `description` | + +### setRoundDataSafe + +```solidity +function setRoundDataSafe(int256 _data) external +``` + +works as `setRoundData()`, but also checks the +deviation with the lattest submitted data + +_deviation with previous data needs to be <= `maxAnswerDeviation`_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _data | int256 | data value | + +### setRoundData + +```solidity +function setRoundData(int256 _data) public +``` + +sets the data for `latestRound` + 1 round id + +_`_data` should be >= `minAnswer` and <= `maxAnswer`. +Function should be called only from address with `feedAdminRole()`_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _data | int256 | data value | + +### latestRoundData + +```solidity +function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +### version + +```solidity +function version() external pure returns (uint256) +``` + +### lastAnswer + +```solidity +function lastAnswer() public view returns (int256) +``` + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | int256 | answer of lattest price submission | + +### lastTimestamp + +```solidity +function lastTimestamp() public view returns (uint256) +``` + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | timestamp of lattest price submission | + +### getRoundData + +```solidity +function getRoundData(uint80 _roundId) public view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +### feedAdminRole + +```solidity +function feedAdminRole() public view virtual returns (bytes32) +``` + +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +### decimals + +```solidity +function decimals() public pure returns (uint8) +``` + +### _getDeviation + +```solidity +function _getDeviation(int256 _lastPrice, int256 _newPrice) internal pure returns (uint256) +``` + +_calculates a deviation in % between `_lastPrice` and `_newPrice`_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | deviation in `10 ** decimals()` precision | + +## CustomAggregatorV3CompatibleFeedDiscounted + +AggregatorV3 compatible proxy-feed that discounts the price +of an underlying chainlink compatible feed by a given percentage + +### underlyingFeed + +```solidity +contract AggregatorV3Interface underlyingFeed +``` + +the underlying chainlink compatible feed + +### discountPercentage + +```solidity +uint256 discountPercentage +``` + +the discount percentage. Expressed in 10 ** decimals() precision +Example: 10 ** decimals() = 1% + +### constructor + +```solidity +constructor(address _underlyingFeed, uint256 _discountPercentage) public +``` + +constructor + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _underlyingFeed | address | the underlying chainlink compatible feed | +| _discountPercentage | uint256 | the discount percentage. Expressed in 10 ** decimals() precision | + +### latestRoundData + +```solidity +function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +### version + +```solidity +function version() external view returns (uint256) +``` + +### getRoundData + +```solidity +function getRoundData(uint80 _roundId) public view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +### decimals + +```solidity +function decimals() public view returns (uint8) +``` + +### description + +```solidity +function description() public view returns (string) +``` + +### _calculateDiscountedAnswer + +```solidity +function _calculateDiscountedAnswer(int256 _answer) internal view returns (int256) +``` + +_calculates the discounted answer_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _answer | int256 | the answer to discount | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | int256 | the discounted answer | + +## CustomAggregatorV3CompatibleFeedGrowth + +AggregatorV3 compatible feed, where price is submitted manually by feed admins +and growth apr % is applied to the answer. + +### RoundDataWithGrowth + +```solidity +struct RoundDataWithGrowth { + uint80 roundId; + uint80 answeredInRound; + int80 growthApr; + int256 answer; + uint256 startedAt; + uint256 updatedAt; +} +``` + +### description + +```solidity +string description +``` + +feed description + +### maxAnswerDeviation + +```solidity +uint256 maxAnswerDeviation +``` + +max deviation from latest price in % + +_10 ** decimals() is a percentage precision_ + +### minAnswer + +```solidity +int192 minAnswer +``` + +minimal possible answer that feed can return + +### maxAnswer + +```solidity +int192 maxAnswer +``` + +maximal possible answer that feed can return + +### minGrowthApr + +```solidity +int80 minGrowthApr +``` + +minimal possible growth apr value that can be set + +### maxGrowthApr + +```solidity +int80 maxGrowthApr +``` + +maximal possible growth apr value that can be set + +### latestRound + +```solidity +uint80 latestRound +``` + +last round id + +### onlyUp + +```solidity +bool onlyUp +``` + +if true, the price can only increase + +_applicable only for setRoundDataSafe_ + +### onlyAggregatorAdmin + +```solidity +modifier onlyAggregatorAdmin() +``` + +_checks that msg.sender do have a feedAdminRole() role_ + +### initialize + +```solidity +function initialize(address _accessControl, int192 _minAnswer, int192 _maxAnswer, uint256 _maxAnswerDeviation, int80 _minGrowthApr, int80 _maxGrowthApr, bool _onlyUp, string _description) external +``` + +upgradeable pattern contract`s initializer + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _accessControl | address | address of MidasAccessControll contract | +| _minAnswer | int192 | init value for `minAnswer`. Should be < `_maxAnswer` | +| _maxAnswer | int192 | init value for `maxAnswer`. Should be > `_minAnswer` | +| _maxAnswerDeviation | uint256 | init value for `maxAnswerDeviation` | +| _minGrowthApr | int80 | init value for `minGrowthApr` | +| _maxGrowthApr | int80 | init value for `maxGrowthApr` | +| _onlyUp | bool | init value for `onlyUp` | +| _description | string | init value for `description` | + +### setOnlyUp + +```solidity +function setOnlyUp(bool _onlyUp) external +``` + +updates onlyUp flag + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _onlyUp | bool | new onlyUp flag | + +### setMaxGrowthApr + +```solidity +function setMaxGrowthApr(int80 _maxGrowthApr) external +``` + +updates max growth apr + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _maxGrowthApr | int80 | new max growth apr | + +### setMinGrowthApr + +```solidity +function setMinGrowthApr(int80 _minGrowthApr) external +``` + +updates min growth apr + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _minGrowthApr | int80 | new min growth apr | + +### setRoundDataSafe + +```solidity +function setRoundDataSafe(int256 _data, uint256 _dataTimestamp, int80 _growthApr) external +``` + +works as `setRoundData()`, but also checks the +deviation with the lattest submitted data + +_deviation with previous data needs to be <= `maxAnswerDeviation`_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _data | int256 | data value | +| _dataTimestamp | uint256 | timestamp of the data in the past | +| _growthApr | int80 | growth apr value | + +### setRoundData + +```solidity +function setRoundData(int256 _data, uint256 _dataTimestamp, int80 _growthApr) public +``` + +sets the data for `latestRound` + 1 round id + +_`_data` should be >= `minAnswer` and <= `maxAnswer`. +Function should be called only from permissioned actor_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _data | int256 | data value | +| _dataTimestamp | uint256 | timestamp of the data in the past | +| _growthApr | int80 | growth apr value | + +### latestRoundData + +```solidity +function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +returns data for latest round with growth applied + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| roundId | uint80 | roundId | +| answer | int256 | answer with growth applied | +| startedAt | uint256 | timestamp passed to setRoundData | +| updatedAt | uint256 | timestamp of the last price submission | +| answeredInRound | uint80 | answeredInRound | + +### latestRoundDataRaw + +```solidity +function latestRoundDataRaw() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound, int80 growthApr) +``` + +returns `latestRoundData` without growth applied + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| roundId | uint80 | roundId | +| answer | int256 | answer with growth applied | +| startedAt | uint256 | startedAt | +| updatedAt | uint256 | updatedAt | +| answeredInRound | uint80 | answeredInRound | +| growthApr | int80 | growthApr | + +### version + +```solidity +function version() external pure returns (uint256) +``` + +### lastAnswer + +```solidity +function lastAnswer() public view returns (int256) +``` + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | int256 | answer of latest price submission | + +### lastGrowthApr + +```solidity +function lastGrowthApr() public view returns (int80) +``` + +returns the growth apr of the latest round + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | int80 | growthApr latest growthApr value | + +### lastTimestamp + +```solidity +function lastTimestamp() public view returns (uint256) +``` + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | `updatedAt` timestamp of latest price submission | + +### lastStartedAt + +```solidity +function lastStartedAt() public view returns (uint256) +``` + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | `startedAt` timestamp of latest price submission | + +### getRoundData + +```solidity +function getRoundData(uint80 _roundId) public view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +returns data for a specific round with growth applied + +_growth to answer is only applied between [roundStartedAt,nextRoundUpdatedAt] +or if roundId is latestRound, block.timestamp will be used as nextRoundUpdatedAt_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _roundId | uint80 | roundId | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| roundId | uint80 | roundId | +| answer | int256 | answer with growth applied | +| startedAt | uint256 | timestamp passed to setRoundData | +| updatedAt | uint256 | timestamp of the last price submission | +| answeredInRound | uint80 | answeredInRound | + +### getRoundDataRaw + +```solidity +function getRoundDataRaw(uint80 _roundId) public view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound, int80 growthApr) +``` + +returns data for a specific round without growth applied + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _roundId | uint80 | roundId | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| roundId | uint80 | roundId | +| answer | int256 | answer with growth applied | +| startedAt | uint256 | startedAt | +| updatedAt | uint256 | updatedAt | +| answeredInRound | uint80 | answeredInRound | +| growthApr | int80 | growthApr value | + +### feedAdminRole + +```solidity +function feedAdminRole() public view virtual returns (bytes32) +``` + +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +### applyGrowth + +```solidity +function applyGrowth(int256 _answer, int80 _growthApr, uint256 _timestampFrom) public view returns (int256) +``` + +applies growth to the answer until current timestamp + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _answer | int256 | answer | +| _growthApr | int80 | growth apr | +| _timestampFrom | uint256 | timestamp from | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | int256 | answer with growth applied | + +### applyGrowth + +```solidity +function applyGrowth(int256 _answer, int80 _growthApr, uint256 _timestampFrom, uint256 _timestampTo) public pure returns (int256) +``` + +applies growth to the answer between two timestamps + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _answer | int256 | answer | +| _growthApr | int80 | growth apr | +| _timestampFrom | uint256 | timestamp from | +| _timestampTo | uint256 | timestamp to | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | int256 | answer with growth applied | + +### decimals + +```solidity +function decimals() public pure returns (uint8) +``` + +### _getDeviation + +```solidity +function _getDeviation(int256 _lastPrice, int256 _newPrice, bool _validateOnlyUp) internal pure returns (uint256) +``` + +_calculates a deviation in % between `_lastPrice` and `_newPrice`_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _lastPrice | int256 | last price | +| _newPrice | int256 | new price | +| _validateOnlyUp | bool | if true, will validate that deviation is positive | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | deviation in `decimals()` precision | + +## DataFeed + +Wrapper of ChainLink`s AggregatorV3 data feeds + +### aggregator + +```solidity +contract AggregatorV3Interface aggregator +``` + +AggregatorV3Interface contract address + +### healthyDiff + +```solidity +uint256 healthyDiff +``` + +_healty difference between `block.timestamp` and `updatedAt` timestamps_ + +### minExpectedAnswer + +```solidity +int256 minExpectedAnswer +``` + +_minimal answer expected to receive from the `aggregator`_ + +### maxExpectedAnswer + +```solidity +int256 maxExpectedAnswer +``` + +_maximal answer expected to receive from the `aggregator`_ + +### initialize + +```solidity +function initialize(address _ac, address _aggregator, uint256 _healthyDiff, int256 _minExpectedAnswer, int256 _maxExpectedAnswer) external +``` + +upgradeable pattern contract`s initializer + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _ac | address | MidasAccessControl contract address | +| _aggregator | address | AggregatorV3Interface contract address | +| _healthyDiff | uint256 | max. staleness time for data feed answers | +| _minExpectedAnswer | int256 | min.expected answer value from data feed | +| _maxExpectedAnswer | int256 | max.expected answer value from data feed | + +### changeAggregator + +```solidity +function changeAggregator(address _aggregator) external +``` + +updates `aggregator` address + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _aggregator | address | new AggregatorV3Interface contract address | + +### setHealthyDiff + +```solidity +function setHealthyDiff(uint256 _healthyDiff) external +``` + +_updates `healthyDiff` value_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _healthyDiff | uint256 | new value | + +### setMinExpectedAnswer + +```solidity +function setMinExpectedAnswer(int256 _minExpectedAnswer) external +``` + +_updates `minExpectedAnswer` value_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _minExpectedAnswer | int256 | min value | + +### setMaxExpectedAnswer + +```solidity +function setMaxExpectedAnswer(int256 _maxExpectedAnswer) external +``` + +_updates `maxExpectedAnswer` value_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _maxExpectedAnswer | int256 | max value | + +### getDataInBase18 + +```solidity +function getDataInBase18() external view returns (uint256 answer) +``` + +fetches answer from aggregator +and converts it to the base18 precision + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| answer | uint256 | fetched aggregator answer | + +### feedAdminRole + +```solidity +function feedAdminRole() public pure virtual returns (bytes32) +``` + +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## IAggregatorV3CompatibleFeedGrowth + +### AnswerUpdated + +```solidity +event AnswerUpdated(int256 data, uint256 roundId, uint256 timestamp, int80 growthApr) +``` + +emitted when answer is updated + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| data | int256 | data value without growth applied | +| roundId | uint256 | roundId | +| timestamp | uint256 | timestamp of the data in the past | +| growthApr | int80 | growthApr value | + +### MaxGrowthAprUpdated + +```solidity +event MaxGrowthAprUpdated(int80 newMaxGrowthApr) +``` + +emitted when max growth apr is updated + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newMaxGrowthApr | int80 | new max growth apr | + +### MinGrowthAprUpdated + +```solidity +event MinGrowthAprUpdated(int80 newMinGrowthApr) +``` + +emitted when min growth apr is updated + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newMinGrowthApr | int80 | new min growth apr | + +### OnlyUpUpdated + +```solidity +event OnlyUpUpdated(bool newOnlyUp) +``` + +emitted when onlyUp flag is updated + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| newOnlyUp | bool | new onlyUp flag | + +### setOnlyUp + +```solidity +function setOnlyUp(bool _onlyUp) external +``` + +updates onlyUp flag + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _onlyUp | bool | new onlyUp flag | + +### setMaxGrowthApr + +```solidity +function setMaxGrowthApr(int80 _maxGrowthApr) external +``` + +updates max growth apr + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _maxGrowthApr | int80 | new max growth apr | + +### setMinGrowthApr + +```solidity +function setMinGrowthApr(int80 _minGrowthApr) external +``` + +updates min growth apr + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _minGrowthApr | int80 | new min growth apr | + +### setRoundDataSafe + +```solidity +function setRoundDataSafe(int256 _data, uint256 _dataTimestamp, int80 _growthApr) external +``` + +works as `setRoundData()`, but also checks the +deviation with the lattest submitted data + +_deviation with previous data needs to be <= `maxAnswerDeviation`_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _data | int256 | data value | +| _dataTimestamp | uint256 | timestamp of the data in the past | +| _growthApr | int80 | growth apr value | + +### setRoundData + +```solidity +function setRoundData(int256 _data, uint256 _dataTimestamp, int80 _growthApr) external +``` + +sets the data for `latestRound` + 1 round id + +_`_data` should be >= `minAnswer` and <= `maxAnswer`. +Function should be called only from permissioned actor_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _data | int256 | data value | +| _dataTimestamp | uint256 | timestamp of the data in the past | +| _growthApr | int80 | growth apr value | + +### latestRoundDataRaw + +```solidity +function latestRoundDataRaw() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound, int80 growthApr) +``` + +returns `latestRoundData` without growth applied + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| roundId | uint80 | roundId | +| answer | int256 | answer with growth applied | +| startedAt | uint256 | startedAt | +| updatedAt | uint256 | updatedAt | +| answeredInRound | uint80 | answeredInRound | +| growthApr | int80 | growthApr | + +### lastGrowthApr + +```solidity +function lastGrowthApr() external view returns (int80) +``` + +returns the growth apr of the latest round + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | int80 | growthApr latest growthApr value | + +### getRoundDataRaw + +```solidity +function getRoundDataRaw(uint80 _roundId) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound, int80 growthApr) +``` + +returns data for a specific round without growth applied + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _roundId | uint80 | roundId | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| roundId | uint80 | roundId | +| answer | int256 | answer with growth applied | +| startedAt | uint256 | startedAt | +| updatedAt | uint256 | updatedAt | +| answeredInRound | uint80 | answeredInRound | +| growthApr | int80 | growthApr value | + +### applyGrowth + +```solidity +function applyGrowth(int256 _answer, int80 _growthApr, uint256 _timestampFrom) external view returns (int256) +``` + +applies growth to the answer until current timestamp + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _answer | int256 | answer | +| _growthApr | int80 | growth apr | +| _timestampFrom | uint256 | timestamp from | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | int256 | answer with growth applied | + +### applyGrowth + +```solidity +function applyGrowth(int256 _answer, int80 _growthApr, uint256 _timestampFrom, uint256 _timestampTo) external pure returns (int256) +``` + +applies growth to the answer between two timestamps + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _answer | int256 | answer | +| _growthApr | int80 | growth apr | +| _timestampFrom | uint256 | timestamp from | +| _timestampTo | uint256 | timestamp to | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | int256 | answer with growth applied | + +## IAllowListV2 + +### EntityId + +### FundPermissionSet + +```solidity +event FundPermissionSet(IAllowListV2.EntityId entityId, string fundSymbol, bool permission) +``` + +An event emitted when an address's permission is changed for a fund. + +### ProtocolAddressPermissionSet + +```solidity +event ProtocolAddressPermissionSet(address addr, string fundSymbol, bool isAllowed) +``` + +An event emitted when a protocol's permission is changed for a fund. + +### EntityIdSet + +```solidity +event EntityIdSet(address addr, uint256 entityId) +``` + +An event emitted when an address is associated with an entityId + +### BadData + +```solidity +error BadData() +``` + +_Thrown when the input for a function is invalid_ + +### AlreadySet + +```solidity +error AlreadySet() +``` + +_Thrown when the input is already equivalent to the storage being set_ + +### NonZeroEntityIdMustBeChangedToZero + +```solidity +error NonZeroEntityIdMustBeChangedToZero() +``` + +_An address's entityId can not be changed once set, it can only be unset and then set to a new value_ + +### AddressHasProtocolPermissions + +```solidity +error AddressHasProtocolPermissions() +``` + +_Thrown when trying to set entityId for an address that has protocol permissions_ + +### AddressHasEntityId + +```solidity +error AddressHasEntityId() +``` + +_Thrown when trying to set protocol permissions for an address that has an entityId_ + +### CodeSizeZero + +```solidity +error CodeSizeZero() +``` + +_Thrown when trying to set protocol permissions but the code size is 0_ + +### Deprecated + +```solidity +error Deprecated() +``` + +_Thrown when a method is no longer supported_ + +### RenounceOwnershipDisabled + +```solidity +error RenounceOwnershipDisabled() +``` + +_Thrown if an attempt to call `renounceOwnership` is made_ + +### owner + +```solidity +function owner() external view returns (address) +``` + +### addressEntityIds + +```solidity +function addressEntityIds(address addr) external view returns (IAllowListV2.EntityId) +``` + +Gets the entityId for the provided address + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| addr | address | The address to get the entityId for | + +### isAddressAllowedForFund + +```solidity +function isAddressAllowedForFund(address addr, string fundSymbol) external view returns (bool) +``` + +Checks whether an address is allowed to use a fund + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| addr | address | The address to check permissions for | +| fundSymbol | string | The fund symbol to check permissions for | + +### isEntityAllowedForFund + +```solidity +function isEntityAllowedForFund(IAllowListV2.EntityId entityId, string fundSymbol) external view returns (bool) +``` + +Checks whether an Entity is allowed to use a fund + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| entityId | IAllowListV2.EntityId | | +| fundSymbol | string | The fund symbol to check permissions for | + +### setEntityAllowedForFund + +```solidity +function setEntityAllowedForFund(IAllowListV2.EntityId entityId, string fundSymbol, bool isAllowed) external +``` + +Sets whether an Entity is allowed to use a fund + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| entityId | IAllowListV2.EntityId | | +| fundSymbol | string | The fund symbol to set permissions for | +| isAllowed | bool | The permission value to set | + +### setEntityIdForAddress + +```solidity +function setEntityIdForAddress(IAllowListV2.EntityId entityId, address addr) external +``` + +Sets the entityId for a given address. Setting to 0 removes the address from the allowList + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| entityId | IAllowListV2.EntityId | The entityId to associate with an address | +| addr | address | The address to associate with an entityId | + +### setEntityIdForMultipleAddresses + +```solidity +function setEntityIdForMultipleAddresses(IAllowListV2.EntityId entityId, address[] addresses) external +``` + +Sets the entity Id for a list of addresses. Setting to 0 removes the address from the allowList + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| entityId | IAllowListV2.EntityId | The entityId to associate with an address | +| addresses | address[] | The addresses to associate with an entityId | + +### setProtocolAddressPermission + +```solidity +function setProtocolAddressPermission(address addr, string fundSymbol, bool isAllowed) external +``` + +Sets protocol permissions for an address + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| addr | address | The address to set permissions for | +| fundSymbol | string | The fund symbol to set permissions for | +| isAllowed | bool | The permission value to set | + +### setProtocolAddressPermissions + +```solidity +function setProtocolAddressPermissions(address[] addresses, string fundSymbol, bool isAllowed) external +``` + +Sets protocol permissions for multiple addresses + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| addresses | address[] | The addresses to set permissions for | +| fundSymbol | string | The fund symbol to set permissions for | +| isAllowed | bool | The permission value to set | + +### setEntityPermissionsAndAddresses + +```solidity +function setEntityPermissionsAndAddresses(IAllowListV2.EntityId entityId, address[] addresses, string[] fundPermissionsToUpdate, bool[] fundPermissions) external +``` + +Sets entity for an array of addresses and sets permissions for an entity + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| entityId | IAllowListV2.EntityId | The entityId to be updated | +| addresses | address[] | The addresses to associate with an entityId | +| fundPermissionsToUpdate | string[] | The funds to update permissions for | +| fundPermissions | bool[] | The permissions for each fund | + +### hasAnyProtocolPermissions + +```solidity +function hasAnyProtocolPermissions(address addr) external view returns (bool hasPermissions) +``` + +### protocolPermissionsForFunds + +```solidity +function protocolPermissionsForFunds(address protocol) external view returns (uint256) +``` + +### protocolPermissions + +```solidity +function protocolPermissions(address, string) external view returns (bool) +``` + +### initialize + +```solidity +function initialize() external +``` + +## mToken + +### metadata + +```solidity +mapping(bytes32 => bytes) metadata +``` + +metadata key => metadata value + +### initialize + +```solidity +function initialize(address _accessControl) external virtual +``` + +upgradeable pattern contract`s initializer + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _accessControl | address | address of MidasAccessControll contract | + +### mint + +```solidity +function mint(address to, uint256 amount) external +``` + +mints mToken token `amount` to a given `to` address. +should be called only from permissioned actor + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| to | address | addres to mint tokens to | +| amount | uint256 | amount to mint | + +### burn + +```solidity +function burn(address from, uint256 amount) external +``` + +burns mToken token `amount` to a given `to` address. +should be called only from permissioned actor + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| from | address | addres to burn tokens from | +| amount | uint256 | amount to burn | + +### pause + +```solidity +function pause() external +``` + +puts mToken token on pause. +should be called only from permissioned actor + +### unpause + +```solidity +function unpause() external +``` + +puts mToken token on pause. +should be called only from permissioned actor + +### setMetadata + +```solidity +function setMetadata(bytes32 key, bytes data) external +``` + +updates contract`s metadata. +should be called only from permissioned actor + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| key | bytes32 | metadata map. key | +| data | bytes | metadata map. value | + +### _beforeTokenTransfer + +```solidity +function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual +``` + +_overrides _beforeTokenTransfer function to ban +blaclisted users from using the token functions_ + +### _getNameSymbol + +```solidity +function _getNameSymbol() internal virtual returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole + +```solidity +function _minterRole() internal pure virtual returns (bytes32) +``` + +_AC role, owner of which can mint mToken token_ + +### _burnerRole + +```solidity +function _burnerRole() internal pure virtual returns (bytes32) +``` + +_AC role, owner of which can burn mToken token_ + +### _pauserRole + +```solidity +function _pauserRole() internal pure virtual returns (bytes32) +``` + +_AC role, owner of which can pause mToken token_ + +## mTokenPermissioned + +mToken with fully permissioned transfers + +### _beforeTokenTransfer + +```solidity +function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual +``` + +_overrides _beforeTokenTransfer function to allow +greenlisted users to use the token transfers functions_ + +### _greenlistedRole + +```solidity +function _greenlistedRole() internal pure virtual returns (bytes32) +``` + +AC role of a greenlist + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role bytes32 role | + +## IStdReference + +### ReferenceData + +A structure returned whenever someone requests for standard reference data. + +```solidity +struct ReferenceData { + uint256 rate; + uint256 lastUpdatedBase; + uint256 lastUpdatedQuote; +} +``` + +### getReferenceData + +```solidity +function getReferenceData(string _base, string _quote) external view returns (struct IStdReference.ReferenceData) +``` + +Returns the price data for the given base/quote pair. Revert if not available. + +### getReferenceDataBulk + +```solidity +function getReferenceDataBulk(string[] _bases, string[] _quotes) external view returns (struct IStdReference.ReferenceData[]) +``` + +Similar to getReferenceData, but with multiple base/quote pairs at once. + +## BandStdChailinkAdapter + +### ref + +```solidity +contract IStdReference ref +``` + +### base + +```solidity +string base +``` + +### quote + +```solidity +string quote +``` + +### constructor + +```solidity +constructor(address _ref, string _base, string _quote) public +``` + +### description + +```solidity +function description() external pure returns (string) +``` + +### latestAnswer + +```solidity +function latestAnswer() public view returns (int256) +``` + +### latestTimestamp + +```solidity +function latestTimestamp() public view returns (uint256) +``` + +### latestRoundData + +```solidity +function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +## IBeHype + +### BeHYPEToHYPE + +```solidity +function BeHYPEToHYPE(uint256 beHYPEAmount) external view returns (uint256) +``` + +## BeHypeChainlinkAdapter + +Adapter for beHYPE LST from hyperbeat for liquidHYPE redemptions + +### beHype + +```solidity +contract IBeHype beHype +``` + +### constructor + +```solidity +constructor(address _beHype) public +``` + +### description + +```solidity +function description() external pure returns (string) +``` + +### latestAnswer + +```solidity +function latestAnswer() public view returns (int256) +``` + +## ChainlinkAdapterBase + +### decimals + +```solidity +function decimals() public view virtual returns (uint8) +``` + +### description + +```solidity +function description() external pure virtual returns (string) +``` + +### version + +```solidity +function version() external view virtual returns (uint256) +``` + +### latestTimestamp + +```solidity +function latestTimestamp() public view virtual returns (uint256) +``` + +### latestRound + +```solidity +function latestRound() public view virtual returns (uint256) +``` + +### latestAnswer + +```solidity +function latestAnswer() public view virtual returns (int256) +``` + +### getAnswer + +```solidity +function getAnswer(uint256) public pure virtual returns (int256) +``` + +### getTimestamp + +```solidity +function getTimestamp(uint256) external pure virtual returns (uint256) +``` + +### getRoundData + +```solidity +function getRoundData(uint80) external view virtual returns (uint80, int256, uint256, uint256, uint80) +``` + +### latestRoundData + +```solidity +function latestRoundData() external view virtual returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +## CompositeDataFeedToBandStdAdapter + +Converts CompositeDataFeed to Band Protocol's IStdReference interface + +_Adapter that wraps CompositeDataFeed to provide Band Protocol standard reference data_ + +### constructor + +```solidity +constructor(address _compositeDataFeed, string _baseSymbol, string _quoteSymbol) public +``` + +Constructor initializes the adapter with a CompositeDataFeed contract + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _compositeDataFeed | address | Address of the CompositeDataFeed contract providing composite price data | +| _baseSymbol | string | Symbol of the base token | +| _quoteSymbol | string | Symbol of the quote currency | + +### _getTimestamp + +```solidity +function _getTimestamp() internal view returns (uint256 timestamp) +``` + +Gets the timestamp for the price data + +_Overrides base to handle composite feeds by taking min timestamp from numerator/denominator_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| timestamp | uint256 | The timestamp of the last price update | + +## IStdReference + +### ReferenceData + +A structure returned whenever someone requests for standard reference data. + +```solidity +struct ReferenceData { + uint256 rate; + uint256 lastUpdatedBase; + uint256 lastUpdatedQuote; +} +``` + +### getReferenceData + +```solidity +function getReferenceData(string _base, string _quote) external view returns (struct IStdReference.ReferenceData) +``` + +Returns the price data for the given base/quote pair. Revert if not available. + +### getReferenceDataBulk + +```solidity +function getReferenceDataBulk(string[] _bases, string[] _quotes) external view returns (struct IStdReference.ReferenceData[]) +``` + +Similar to getReferenceData, but with multiple base/quote pairs at once. + +## DataFeedToBandStdAdapter + +Converts DataFeed to Band Protocol's IStdReference interface + +_Base adapter that wraps a DataFeed to provide Band Protocol standard reference data_ + +### dataFeed + +```solidity +contract IDataFeed dataFeed +``` + +DataFeed contract providing validated price data + +### baseSymbol + +```solidity +string baseSymbol +``` + +Base token symbol + +### quoteSymbol + +```solidity +string quoteSymbol +``` + +Quote currency symbol + +### constructor + +```solidity +constructor(address _dataFeed, string _baseSymbol, string _quoteSymbol) public +``` + +Constructor initializes the adapter with a DataFeed contract + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _dataFeed | address | Address of the DataFeed contract providing price data | +| _baseSymbol | string | Symbol of the base token | +| _quoteSymbol | string | Symbol of the quote currency | + +### getReferenceData + +```solidity +function getReferenceData(string _base, string _quote) external view returns (struct IStdReference.ReferenceData) +``` + +Returns the price data for the given base/quote pair + +_Only supports the configured baseSymbol/quoteSymbol pair_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _base | string | The base token symbol | +| _quote | string | The quote currency symbol | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | struct IStdReference.ReferenceData | ReferenceData containing rate and update timestamps | + +### getReferenceDataBulk + +```solidity +function getReferenceDataBulk(string[] _bases, string[] _quotes) external view returns (struct IStdReference.ReferenceData[]) +``` + +Returns price data for multiple base/quote pairs + +_Only supports single pair queries (array length must be 1)_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _bases | string[] | Array of base token symbols (must have length 1) | +| _quotes | string[] | Array of quote currency symbols (must have length 1) | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | struct IStdReference.ReferenceData[] | Array containing single ReferenceData element | + +### _getTimestamp + +```solidity +function _getTimestamp() internal view virtual returns (uint256 timestamp) +``` + +Gets the timestamp for the price data + +_Virtual function that can be overridden by child contracts_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| timestamp | uint256 | The timestamp of the last price update | + +### _getAggregatorTimestamp + +```solidity +function _getAggregatorTimestamp(contract IDataFeed feed) internal view returns (uint256) +``` + +Gets timestamp from a DataFeed via its aggregator + +_Assumes the feed is a DataFeed. Reverts if not._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| feed | contract IDataFeed | The data feed to get timestamp from | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint256 | timestamp The timestamp from the aggregator | + +## ERC4626ChainlinkAdapter + +_uses convertToAssets for the answer_ + +### vault + +```solidity +address vault +``` + +erc4626 vault + +### constructor + +```solidity +constructor(address _vault) public +``` + +_constructor_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _vault | address | erc4626 vault address | + +### description + +```solidity +function description() external pure virtual returns (string) +``` + +### decimals + +```solidity +function decimals() public view returns (uint8) +``` + +### vaultDecimals + +```solidity +function vaultDecimals() public view returns (uint8) +``` + +### latestAnswer + +```solidity +function latestAnswer() public view virtual returns (int256) +``` + +## IMantleLspStaking + +### mETHToETH + +```solidity +function mETHToETH(uint256 mETHAmount) external view returns (uint256) +``` + +## MantleLspStakingChainlinkAdapter + +example https://etherscan.io/address/0xe3cBd06D7dadB3F4e6557bAb7EdD924CD1489E8f + +### lspStaking + +```solidity +contract IMantleLspStaking lspStaking +``` + +### constructor + +```solidity +constructor(address _lspStaking) public +``` + +### description + +```solidity +function description() external pure returns (string) +``` + +### latestAnswer + +```solidity +function latestAnswer() public view returns (int256) +``` + +## PythStructs + +### Price + +```solidity +struct Price { + int64 price; + uint64 conf; + int32 expo; + uint256 publishTime; +} +``` + +## IPyth + +### getPriceUnsafe + +```solidity +function getPriceUnsafe(bytes32 id) external view returns (struct PythStructs.Price price) +``` + +### getUpdateFee + +```solidity +function getUpdateFee(bytes[] updateData) external view returns (uint256 feeAmount) +``` + +### updatePriceFeeds + +```solidity +function updatePriceFeeds(bytes[] updateData) external payable +``` + +## PythChainlinkAdapter + +### priceId + +```solidity +bytes32 priceId +``` + +### pyth + +```solidity +contract IPyth pyth +``` + +### constructor + +```solidity +constructor(address _pyth, bytes32 _priceId) public +``` + +### updateFeeds + +```solidity +function updateFeeds(bytes[] priceUpdateData) public payable +``` + +### decimals + +```solidity +function decimals() public view virtual returns (uint8) +``` + +### description + +```solidity +function description() external pure returns (string) +``` + +### latestAnswer + +```solidity +function latestAnswer() public view virtual returns (int256) +``` + +### latestTimestamp + +```solidity +function latestTimestamp() public view returns (uint256) +``` + +### latestRoundData + +```solidity +function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +## IRsEth + +### rsETHPrice + +```solidity +function rsETHPrice() external view returns (uint256) +``` + +## RsEthChainlinkAdapter + +example https://etherscan.io/address/0x349A73444b1a310BAe67ef67973022020d70020d + +### rsEth + +```solidity +contract IRsEth rsEth +``` + +### constructor + +```solidity +constructor(address _rsEth) public +``` + +### description + +```solidity +function description() external pure returns (string) +``` + +### latestAnswer + +```solidity +function latestAnswer() public view returns (int256) +``` + +## IStorkTemporalNumericValueUnsafeGetter + +### getTemporalNumericValueUnsafeV1 + +```solidity +function getTemporalNumericValueUnsafeV1(bytes32 id) external view returns (struct StorkStructs.TemporalNumericValue value) +``` + +## StorkStructs + +### TemporalNumericValue + +```solidity +struct TemporalNumericValue { + uint64 timestampNs; + int192 quantizedValue; +} +``` + +## StorkChainlinkAdapter + +### TIMESTAMP_DIVIDER + +```solidity +uint256 TIMESTAMP_DIVIDER +``` + +### priceId + +```solidity +bytes32 priceId +``` + +### stork + +```solidity +contract IStorkTemporalNumericValueUnsafeGetter stork +``` + +### constructor + +```solidity +constructor(address _stork, bytes32 _priceId) public +``` + +### description + +```solidity +function description() external pure returns (string) +``` + +### latestAnswer + +```solidity +function latestAnswer() public view returns (int256) +``` + +### latestTimestamp + +```solidity +function latestTimestamp() public view returns (uint256) +``` + +### latestRoundData + +```solidity +function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +## ISyrupToken + +### convertToExitAssets + +```solidity +function convertToExitAssets(uint256 shares) external view returns (uint256) +``` + +## SyrupChainlinkAdapter + +example https://etherscan.io/address/0x80ac24aa929eaf5013f6436cda2a7ba190f5cc0b + +### constructor + +```solidity +constructor(address _syrupToken) public +``` + +### description + +```solidity +function description() external pure returns (string) +``` + +### latestAnswer + +```solidity +function latestAnswer() public view returns (int256) +``` + +## IWrappedEEth + +### getRate + +```solidity +function getRate() external view returns (uint256) +``` + +## WrappedEEthChainlinkAdapter + +example https://etherscan.io/address/0xcd5fe23c85820f7b72d0926fc9b05b43e359b7ee + +### wrappedEEth + +```solidity +contract IWrappedEEth wrappedEEth +``` + +### constructor + +```solidity +constructor(address _wrappedEEth) public +``` + +### description + +```solidity +function description() external pure returns (string) +``` + +### latestAnswer + +```solidity +function latestAnswer() public view returns (int256) +``` + +## IWstEth + +### stEthPerToken + +```solidity +function stEthPerToken() external view returns (uint256) +``` + +## WstEthChainlinkAdapter + +example https://etherscan.io/address/0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0 + +### wstEth + +```solidity +contract IWstEth wstEth +``` + +### constructor + +```solidity +constructor(address _wstEth) public +``` + +### description + +```solidity +function description() external pure returns (string) +``` + +### latestAnswer + +```solidity +function latestAnswer() public view returns (int256) +``` + +## IYInjOracle + +### getExchangeRate + +```solidity +function getExchangeRate() external view returns (uint256) +``` + +## YInjChainlinkAdapter + +Adapter for yINJ from injective for sLINJ redemptions + +### yInj + +```solidity +contract IYInjOracle yInj +``` + +### constructor + +```solidity +constructor(address _yINJ) public +``` + +### description + +```solidity +function description() external pure returns (string) +``` + +### latestAnswer + +```solidity +function latestAnswer() public view returns (int256) +``` + +## MidasLzMintBurnOFTAdapter + +OFT MintBurn adapter implementation + +### SenderNotThis + +```solidity +error SenderNotThis(address sender) +``` + +error thrown when the sender is not the contract + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| sender | address | the address of the sender | + +### onlyThis + +```solidity +modifier onlyThis() +``` + +modifier to check if the sender is the contract itself + +### constructor + +```solidity +constructor(address _token, address _lzEndpoint, address _delegate, struct RateLimiter.RateLimitConfig[] _rateLimitConfigs) public +``` + +constructor + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _token | address | address of the mToken | +| _lzEndpoint | address | address of the LayerZero endpoint | +| _delegate | address | address of the delegate | +| _rateLimitConfigs | struct RateLimiter.RateLimitConfig[] | | + +### burn + +```solidity +function burn(address _from, uint256 _amount) external returns (bool) +``` + +Burns tokens from a specified account + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _from | address | Address from which tokens will be burned | +| _amount | uint256 | Amount of tokens to be burned | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | | + +### mint + +```solidity +function mint(address _to, uint256 _amount) external returns (bool) +``` + +Mints tokens to a specified account + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _to | address | Address to which tokens will be minted | +| _amount | uint256 | Amount of tokens to be minted | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bool | | + +### setRateLimits + +```solidity +function setRateLimits(struct RateLimiter.RateLimitConfig[] _rateLimitConfigs) external +``` + +Sets the rate limits for the adapter + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _rateLimitConfigs | struct RateLimiter.RateLimitConfig[] | the rate limit configs to set | + +### getRateLimit + +```solidity +function getRateLimit(uint32 _dstEid) external view returns (struct RateLimiter.RateLimit) +``` + +Returns the rate limit for a given destination EID + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _dstEid | uint32 | the destination EID | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | struct RateLimiter.RateLimit | the rate limit struct | + +### sharedDecimals + +```solidity +function sharedDecimals() public pure returns (uint8) +``` + +Returns the shared decimals for the adapter + +_Overridden to 9 because default is not enough for +some of the mTokens_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint8 | The shared decimals | + +### _debit + +```solidity +function _debit(address _from, uint256 _amountLD, uint256 _minAmountLD, uint32 _dstEid) internal returns (uint256 amountSentLD, uint256 amountReceivedLD) +``` + +Burns tokens from the sender's balance to prepare for sending. + +_WARNING: The default OFTAdapter implementation assumes LOSSLESS transfers, i.e., 1 token in, 1 token out. + If the 'innerToken' applies something like a transfer fee, the default will NOT work. + A pre/post balance check will need to be done to calculate the amountReceivedLD._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _from | address | The address to debit the tokens from. | +| _amountLD | uint256 | The amount of tokens to send in local decimals. | +| _minAmountLD | uint256 | The minimum amount to send in local decimals. | +| _dstEid | uint32 | The destination chain ID. | + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| amountSentLD | uint256 | The amount sent in local decimals. | +| amountReceivedLD | uint256 | The amount received in local decimals on the remote. | + +## MidasLzOFT + +OFT adapter implementation + +### constructor + +```solidity +constructor(string _name, string _symbol, uint8 __sharedDecimals, address _lzEndpoint, address _delegate) public +``` + +constructor + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _name | string | name of the token | +| _symbol | string | symbol of the token | +| __sharedDecimals | uint8 | shared decimals for the OFT | +| _lzEndpoint | address | address of the LayerZero endpoint | +| _delegate | address | address of the delegate | + +### sharedDecimals + +```solidity +function sharedDecimals() public view returns (uint8) +``` + +Returns the shared decimals for the OFT + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint8 | The shared decimals | + +## MidasLzOFTAdapter + +OFT adapter implementation + +### constructor + +```solidity +constructor(address _token, uint8 __sharedDecimals, address _lzEndpoint, address _delegate) public +``` + +constructor + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _token | address | address of the token | +| __sharedDecimals | uint8 | shared decimals for the OFT adapter | +| _lzEndpoint | address | address of the LayerZero endpoint | +| _delegate | address | address of the delegate | + +### sharedDecimals + +```solidity +function sharedDecimals() public view returns (uint8) +``` + +Returns the shared decimals for the OFT + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | uint8 | The shared decimals | + +## AaveV3PoolMock + +### reserveATokens + +```solidity +mapping(address => address) reserveATokens +``` + +### withdrawReturnBps + +```solidity +uint256 withdrawReturnBps +``` + +### shouldRevertSupply + +```solidity +bool shouldRevertSupply +``` + +### setReserveAToken + +```solidity +function setReserveAToken(address asset, address aToken) external +``` + +### setWithdrawReturnBps + +```solidity +function setWithdrawReturnBps(uint256 bps) external +``` + +### withdraw + +```solidity +function withdraw(address asset, uint256 amount, address to) external returns (uint256) +``` + +### setShouldRevertSupply + +```solidity +function setShouldRevertSupply(bool _shouldRevert) external +``` + +### supply + +```solidity +function supply(address asset, uint256 amount, address onBehalfOf, uint16) external +``` + +### withdrawAdmin + +```solidity +function withdrawAdmin(address token, address to, uint256 amount) external +``` + +### getReserveAToken + +```solidity +function getReserveAToken(address asset) external view returns (address) +``` + +## AggregatorV3DeprecatedMock + +### decimals + +```solidity +function decimals() external view returns (uint8) +``` + +### description + +```solidity +function description() external view returns (string) +``` + +### version + +```solidity +function version() external view returns (uint256) +``` + +### setRoundData + +```solidity +function setRoundData(int256 _data) external +``` + +### getRoundData + +```solidity +function getRoundData(uint80 _roundId) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +### latestRoundData + +```solidity +function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +## AggregatorV3Mock + +### decimals + +```solidity +function decimals() external view returns (uint8) +``` + +### description + +```solidity +function description() external view returns (string) +``` + +### version + +```solidity +function version() external view returns (uint256) +``` + +### setRoundData + +```solidity +function setRoundData(int256 _data) external +``` + +### getRoundData + +```solidity +function getRoundData(uint80 _roundId) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +### latestRoundData + +```solidity +function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +## AggregatorV3UnhealthyMock + +### decimals + +```solidity +function decimals() external view returns (uint8) +``` + +### description + +```solidity +function description() external view returns (string) +``` + +### version + +```solidity +function version() external view returns (uint256) +``` + +### setRoundData + +```solidity +function setRoundData(int256 _data) external +``` + +### getRoundData + +```solidity +function getRoundData(uint80 _roundId) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +### latestRoundData + +```solidity +function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +``` + +## IERC20MintBurn + +### mint + +```solidity +function mint(address to, uint256 amount) external +``` + +### burn + +```solidity +function burn(address from, uint256 amount) external +``` + +## AxelarInterchainTokenServiceMock + +### registeredTokenAddresses + +```solidity +mapping(bytes32 => address) registeredTokenAddresses +``` + +### mintBurn + +```solidity +mapping(bytes32 => bool) mintBurn +``` + +### shouldRevert + +```solidity +bool shouldRevert +``` + +### chainNameHash + +```solidity +bytes32 chainNameHash +``` + +### setChainNameHash + +```solidity +function setChainNameHash(bytes32 _chainNameHash) external +``` + +### setShouldRevert + +```solidity +function setShouldRevert(bool _shouldRevert) external +``` + +### registerToken + +```solidity +function registerToken(bytes32 tokenId, address tokenAddress, bool _mintBurn) external +``` + +### interchainTransfer + +```solidity +function interchainTransfer(bytes32 tokenId, string, bytes destinationAddressBytes, uint256 amount, bytes, uint256) external payable +``` + +### callContractWithInterchainToken + +```solidity +function callContractWithInterchainToken(bytes32 tokenId, string destinationChain, bytes destinationAddress, uint256 amount, bytes data) external payable +``` + +### registeredTokenAddress + +```solidity +function registeredTokenAddress(bytes32 tokenId) external view returns (address tokenAddress) +``` + +## ERC20Mock + +### constructor + +```solidity +constructor(uint8 decimals_) public +``` + +### mint + +```solidity +function mint(address to, uint256 amount) external +``` + +### burn + +```solidity +function burn(address from, uint256 amount) external +``` + +### decimals + +```solidity +function decimals() public view returns (uint8) +``` + +_Returns the number of decimals used to get its user representation. +For example, if `decimals` equals `2`, a balance of `505` tokens should +be displayed to a user as `5.05` (`505 / 10 ** 2`). + +Tokens usually opt for a value of 18, imitating the relationship between +Ether and Wei. This is the value {ERC20} uses, unless this function is +overridden; + +NOTE: This information is only used for _display_ purposes: it in +no way affects any of the arithmetic of the contract, including +{IERC20-balanceOf} and {IERC20-transfer}._ + +## ERC20MockWithName + +### constructor + +```solidity +constructor(uint8 decimals_, string name, string symb) public +``` + +### mint + +```solidity +function mint(address to, uint256 amount) external +``` + +### decimals + +```solidity +function decimals() public view returns (uint8) +``` + +_Returns the number of decimals used to get its user representation. +For example, if `decimals` equals `2`, a balance of `505` tokens should +be displayed to a user as `5.05` (`505 / 10 ** 2`). + +Tokens usually opt for a value of 18, imitating the relationship between +Ether and Wei. This is the value {ERC20} uses, unless this function is +overridden; + +NOTE: This information is only used for _display_ purposes: it in +no way affects any of the arithmetic of the contract, including +{IERC20-balanceOf} and {IERC20-transfer}._ + +## LzEndpointV2Mock + +### EMPTY_PAYLOAD_HASH + +```solidity +bytes32 EMPTY_PAYLOAD_HASH +``` + +### eid + +```solidity +uint32 eid +``` + +### lzEndpointLookup + +```solidity +mapping(address => address) lzEndpointLookup +``` + +### readResponseLookup + +```solidity +mapping(address => bytes) readResponseLookup +``` + +### readChannelId + +```solidity +uint32 readChannelId +``` + +### lazyInboundNonce + +```solidity +mapping(address => mapping(uint32 => mapping(bytes32 => uint64))) lazyInboundNonce +``` + +### inboundPayloadHash + +```solidity +mapping(address => mapping(uint32 => mapping(bytes32 => mapping(uint64 => bytes32)))) inboundPayloadHash +``` + +### outboundNonce + +```solidity +mapping(address => mapping(uint32 => mapping(bytes32 => uint64))) outboundNonce +``` + +### nextComposerMsgValue + +```solidity +uint256 nextComposerMsgValue +``` + +### relayerFeeConfig + +```solidity +struct LzEndpointV2Mock.RelayerFeeConfig relayerFeeConfig +``` + +### protocolFeeConfig + +```solidity +struct LzEndpointV2Mock.ProtocolFeeConfig protocolFeeConfig +``` + +### verifierFee + +```solidity +uint256 verifierFee +``` + +### ProtocolFeeConfig + +```solidity +struct ProtocolFeeConfig { + uint256 zroFee; + uint256 nativeBP; +} +``` + +### RelayerFeeConfig + +```solidity +struct RelayerFeeConfig { + uint128 dstPriceRatio; + uint128 dstGasPriceInWei; + uint128 dstNativeAmtCap; + uint64 baseGas; + uint64 gasPerByte; +} +``` + +### _NOT_ENTERED + +```solidity +uint8 _NOT_ENTERED +``` + +### _ENTERED + +```solidity +uint8 _ENTERED +``` + +### _receive_entered_state + +```solidity +uint8 _receive_entered_state +``` + +### receiveNonReentrant + +```solidity +modifier receiveNonReentrant() +``` + +### ValueTransferFailed + +```solidity +event ValueTransferFailed(address to, uint256 quantity) +``` + +### constructor + +```solidity +constructor(uint32 _eid) public +``` + +### send + +```solidity +function send(struct MessagingParams _params, address _refundAddress) public payable returns (struct MessagingReceipt receipt) +``` + +### receivePayload + +```solidity +function receivePayload(struct Origin _origin, address _receiver, bytes32 _payloadHash, bytes _message, uint256 _gas, uint256 _msgValue, bytes32 _guid) external payable +``` + +### getExecutorFee + +```solidity +function getExecutorFee(uint256 _payloadSize, bytes _options) public view returns (uint256) +``` + +### _quote + +```solidity +function _quote(struct MessagingParams _params, address) internal view returns (struct MessagingFee messagingFee) +``` + +### _getTreasuryAndVerifierFees + +```solidity +function _getTreasuryAndVerifierFees(uint256 _executorFee, uint256 _verifierFee) internal view returns (uint256) +``` + +### _outbound + +```solidity +function _outbound(address _sender, uint32 _dstEid, bytes32 _receiver) internal returns (uint64 nonce) +``` + +### setDestLzEndpoint + +```solidity +function setDestLzEndpoint(address destAddr, address lzEndpointAddr) external +``` + +### setReadResponse + +```solidity +function setReadResponse(address destAddr, bytes resolvedPayload) external +``` + +### setReadChannelId + +```solidity +function setReadChannelId(uint32 _readChannelId) external +``` + +### _decodeExecutorOptions + +```solidity +function _decodeExecutorOptions(bytes _options) internal view returns (uint256 dstAmount, uint256 totalGas) +``` + +### splitOptions + +```solidity +function splitOptions(bytes _options) internal pure returns (bytes, struct WorkerOptions[]) +``` + +### decode + +```solidity +function decode(bytes _options) internal pure returns (bytes executorOptions, bytes dvnOptions) +``` + +### decodeLegacyOptions + +```solidity +function decodeLegacyOptions(uint16 _optionType, bytes _options) internal pure returns (bytes executorOptions) +``` + +### burn + +```solidity +function burn(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes32 _payloadHash) external +``` + +### clear + +```solidity +function clear(address _oapp, struct Origin _origin, bytes32 _guid, bytes _message) external +``` + +### composeQueue + +```solidity +mapping(address => mapping(address => mapping(bytes32 => mapping(uint16 => bytes32)))) composeQueue +``` + +### defaultReceiveLibrary + +```solidity +function defaultReceiveLibrary(uint32) external pure returns (address) +``` + +### defaultReceiveLibraryTimeout + +```solidity +function defaultReceiveLibraryTimeout(uint32) external pure returns (address lib, uint256 expiry) +``` + +### defaultSendLibrary + +```solidity +function defaultSendLibrary(uint32) external pure returns (address) +``` + +### executable + +```solidity +function executable(struct Origin, address) external pure returns (enum ExecutionState) +``` + +### getConfig + +```solidity +function getConfig(address, address, uint32, uint32) external pure returns (bytes config) +``` + +### getReceiveLibrary + +```solidity +function getReceiveLibrary(address, uint32) external pure returns (address lib, bool isDefault) +``` + +### getRegisteredLibraries + +```solidity +function getRegisteredLibraries() external pure returns (address[]) +``` + +### getSendLibrary + +```solidity +function getSendLibrary(address, uint32) external pure returns (address lib) +``` + +### inboundNonce + +```solidity +function inboundNonce(address _receiver, uint32 _srcEid, bytes32 _sender) external view returns (uint64) +``` + +### isDefaultSendLibrary + +```solidity +function isDefaultSendLibrary(address, uint32) external pure returns (bool) +``` + +### isRegisteredLibrary + +```solidity +function isRegisteredLibrary(address) external pure returns (bool) +``` + +### isSupportedEid + +```solidity +function isSupportedEid(uint32) external pure returns (bool) +``` + +### lzCompose + +```solidity +function lzCompose(address, address, bytes32, uint16, bytes, bytes) external payable +``` + +### lzReceive + +```solidity +function lzReceive(struct Origin, address, bytes32, bytes, bytes) external payable +``` + +### lzToken + +```solidity +function lzToken() external pure returns (address) +``` + +### nativeToken + +```solidity +function nativeToken() external pure returns (address) +``` + +### nextGuid + +```solidity +function nextGuid(address, uint32, bytes32) external pure returns (bytes32) +``` + +### nilify + +```solidity +function nilify(address, uint32, bytes32, uint64, bytes32) external +``` + +### quote + +```solidity +function quote(struct MessagingParams _params, address _sender) external view returns (struct MessagingFee) +``` + +### receiveLibraryTimeout + +```solidity +mapping(address => mapping(uint32 => struct IMessageLibManager.Timeout)) receiveLibraryTimeout +``` + +### registerLibrary + +```solidity +function registerLibrary(address) public +``` + +### setNextComposerMsgValue + +```solidity +function setNextComposerMsgValue() external payable +``` + +### sendCompose + +```solidity +function sendCompose(address to, bytes32 guid, uint16, bytes message) external +``` + +### setConfig + +```solidity +function setConfig(address, address, struct SetConfigParam[]) external +``` + +### setDefaultReceiveLibrary + +```solidity +function setDefaultReceiveLibrary(uint32, address, uint256) external +``` + +### setDefaultReceiveLibraryTimeout + +```solidity +function setDefaultReceiveLibraryTimeout(uint32, address, uint256) external +``` + +### setDefaultSendLibrary + +```solidity +function setDefaultSendLibrary(uint32, address) external +``` + +### setDelegate + +```solidity +function setDelegate(address) external +``` + +### setLzToken + +```solidity +function setLzToken(address) external +``` + +### setReceiveLibrary + +```solidity +function setReceiveLibrary(address, uint32, address, uint256) external +``` + +### setReceiveLibraryTimeout + +```solidity +function setReceiveLibraryTimeout(address, uint32, address, uint256) external +``` + +### setSendLibrary + +```solidity +function setSendLibrary(address, uint32, address) external +``` + +### skip + +```solidity +function skip(address, uint32, bytes32, uint64) external +``` + +### verifiable + +```solidity +function verifiable(struct Origin, address, address, bytes32) external pure returns (bool) +``` + +### verify + +```solidity +function verify(struct Origin, address, bytes32) external +``` + +### executeNativeAirDropAndReturnLzGas + +```solidity +function executeNativeAirDropAndReturnLzGas(bytes _options) public returns (uint256 totalGas, uint256 dstAmount) +``` + +### _executeNativeAirDropAndReturnLzGas + +```solidity +function _executeNativeAirDropAndReturnLzGas(bytes _options) public returns (uint256 totalGas, uint256 dstAmount) +``` + +### _initializable + +```solidity +function _initializable(struct Origin _origin, address _receiver, uint64 _lazyInboundNonce) internal view returns (bool) +``` + +### _verifiable + +```solidity +function _verifiable(struct Origin _origin, address _receiver, uint64 _lazyInboundNonce) internal view returns (bool) +``` + +_bytes(0) payloadHash can never be submitted_ + +### initializable + +```solidity +function initializable(struct Origin _origin, address _receiver) external view returns (bool) +``` + +### verifiable + +```solidity +function verifiable(struct Origin _origin, address _receiver) external view returns (bool) +``` + +### isValidReceiveLibrary + +```solidity +function isValidReceiveLibrary(address _receiver, uint32 _srcEid, address _actualReceiveLib) public view returns (bool) +``` + +_called when the endpoint checks if the msgLib attempting to verify the msg is the configured msgLib of the Oapp +this check provides the ability for Oapp to lock in a trusted msgLib +it will fist check if the msgLib is the currently configured one. then check if the msgLib is the one in grace period of msgLib versioning upgrade_ + +### fallback + +```solidity +fallback() external payable +``` + +### receive + +```solidity +receive() external payable +``` + +## MorphoVaultMock + +### underlyingAsset + +```solidity +address underlyingAsset +``` + +### exchangeRateNumerator + +```solidity +uint256 exchangeRateNumerator +``` + +### RATE_PRECISION + +```solidity +uint256 RATE_PRECISION +``` + +### shouldRevertDeposit + +```solidity +bool shouldRevertDeposit +``` + +### constructor + +```solidity +constructor(address _underlyingAsset) public +``` + +### mint + +```solidity +function mint(address to, uint256 amount) external +``` + +### setExchangeRate + +```solidity +function setExchangeRate(uint256 _numerator) external +``` + +### setShouldRevertDeposit + +```solidity +function setShouldRevertDeposit(bool _shouldRevert) external +``` + +### withdrawAdmin + +```solidity +function withdrawAdmin(address token, address to, uint256 amount) external +``` + +### asset + +```solidity +function asset() external view returns (address) +``` + +### deposit + +```solidity +function deposit(uint256 assets, address receiver) external returns (uint256 shares) +``` + +### previewDeposit + +```solidity +function previewDeposit(uint256 assets) public view returns (uint256 shares) +``` + +### redeem + +```solidity +function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets) +``` + +### withdraw + +```solidity +function withdraw(uint256 assets, address receiver, address owner) public returns (uint256 shares) +``` + +### previewWithdraw + +```solidity +function previewWithdraw(uint256 assets) public view returns (uint256 shares) +``` + +### convertToAssets + +```solidity +function convertToAssets(uint256 shares) public view returns (uint256 assets) +``` + +## SanctionsListMock + +### isSanctioned + +```solidity +mapping(address => bool) isSanctioned +``` + +### setSanctioned + +```solidity +function setSanctioned(address addr, bool sanctioned) external +``` + +## USTBMock + +### owner + +```solidity +address owner +``` + +### constructor + +```solidity +constructor() public +``` + +### symbol + +```solidity +function symbol() public view returns (string) +``` + +### decimals + +```solidity +function decimals() public view returns (uint8) +``` + +_Returns the number of decimals used to get its user representation. +For example, if `decimals` equals `2`, a balance of `505` tokens should +be displayed to a user as `5.05` (`505 / 10 ** 2`). + +Tokens usually opt for a value of 18, imitating the relationship between +Ether and Wei. This is the value {ERC20} uses, unless this function is +overridden; + +NOTE: This information is only used for _display_ purposes: it in +no way affects any of the arithmetic of the contract, including +{IERC20-balanceOf} and {IERC20-transfer}._ + +### mint + +```solidity +function mint(address to, uint256 amount) external +``` + +### subscribe + +```solidity +function subscribe(address to, uint256 inAmount, address stablecoin) public +``` + +### subscribe + +```solidity +function subscribe(uint256 inAmount, address stablecoin) external +``` + +### setStablecoinConfig + +```solidity +function setStablecoinConfig(address stablecoin, address newSweepDestination, uint96 newFee) external +``` + +### setAllowListV2 + +```solidity +function setAllowListV2(address allowListV2_) external +``` + +### setIsAllowed + +```solidity +function setIsAllowed(address addr, bool isAllowed_) external +``` + +### _subscribe + +```solidity +function _subscribe(address to, uint256 inAmount, address stablecoin) internal +``` + +_mints ustb 1:1 to inAmount_ + +### supportedStablecoins + +```solidity +function supportedStablecoins(address stablecoin) public view returns (struct ISuperstateToken.StablecoinConfig) +``` + +### allowListV2 + +```solidity +function allowListV2() external view returns (address) +``` + +### isAllowed + +```solidity +function isAllowed(address addr) external view returns (bool) +``` + +## USTBRedemptionMock + +### USDC_DECIMALS + +```solidity +uint256 USDC_DECIMALS +``` + +### USDC_PRECISION + +```solidity +uint256 USDC_PRECISION +``` + +### SUPERSTATE_TOKEN_DECIMALS + +```solidity +uint256 SUPERSTATE_TOKEN_DECIMALS +``` + +### SUPERSTATE_TOKEN_PRECISION + +```solidity +uint256 SUPERSTATE_TOKEN_PRECISION +``` + +### FEE_DENOMINATOR + +```solidity +uint256 FEE_DENOMINATOR +``` + +### CHAINLINK_FEED_PRECISION + +```solidity +uint256 CHAINLINK_FEED_PRECISION +``` + +### SUPERSTATE_TOKEN + +```solidity +contract IERC20 SUPERSTATE_TOKEN +``` + +### USDC + +```solidity +contract IERC20 USDC +``` + +### redemptionFee + +```solidity +uint256 redemptionFee +``` + +### _maxUstbRedemptionAmount + +```solidity +uint256 _maxUstbRedemptionAmount +``` + +### constructor + +```solidity +constructor(address ustbToken, address usdcToken) public +``` + +### calculateFee + +```solidity +function calculateFee(uint256 amount) public view returns (uint256) +``` + +### calculateUstbIn + +```solidity +function calculateUstbIn(uint256 usdcOutAmount) public view returns (uint256 ustbInAmount, uint256 usdPerUstbChainlinkRaw) +``` + +### calculateUsdcOut + +```solidity +function calculateUsdcOut(uint256 superstateTokenInAmount) external view returns (uint256 usdcOutAmountAfterFee, uint256 usdPerUstbChainlinkRaw) +``` + +### _calculateUsdcOut + +```solidity +function _calculateUsdcOut(uint256 superstateTokenInAmount) internal view returns (uint256 usdcOutAmountAfterFee, uint256 usdcOutAmountBeforeFee, uint256 usdPerUstbChainlinkRaw) +``` + +### maxUstbRedemptionAmount + +```solidity +function maxUstbRedemptionAmount() external view returns (uint256 superstateTokenAmount, uint256 usdPerUstbChainlinkRaw) +``` + +### redeem + +```solidity +function redeem(uint256 superstateTokenInAmount) external +``` + +### redeem + +```solidity +function redeem(address to, uint256 superstateTokenInAmount) external +``` + +### _redeem + +```solidity +function _redeem(address to, uint256 superstateTokenInAmount) internal +``` + +### withdraw + +```solidity +function withdraw(address _token, address to, uint256 amount) external +``` + +### _getChainlinkPrice + +```solidity +function _getChainlinkPrice() internal view returns (bool _isBadData, uint256 _updatedAt, uint256 _price) +``` + +### _requireNotPaused + +```solidity +function _requireNotPaused() internal view +``` + +### setRedemptionFee + +```solidity +function setRedemptionFee(uint256 fee) external +``` + +### setChainlinkData + +```solidity +function setChainlinkData(uint256 price, bool isBadData) external +``` + +### setPaused + +```solidity +function setPaused(bool paused) external +``` + +### setMaxUstbRedemptionAmount + +```solidity +function setMaxUstbRedemptionAmount(uint256 maxUstbRedemptionAmount_) external +``` + +## YInjOracleMock + +### constructor + +```solidity +constructor(uint256 _rate) public +``` + +### getExchangeRate + +```solidity +function getExchangeRate() external view returns (uint256) +``` + +## JIV + +### JIV_MINT_OPERATOR_ROLE + +```solidity +bytes32 JIV_MINT_OPERATOR_ROLE +``` + +actor that can mint JIV + +### JIV_BURN_OPERATOR_ROLE + +```solidity +bytes32 JIV_BURN_OPERATOR_ROLE +``` + +actor that can burn JIV + +### JIV_PAUSE_OPERATOR_ROLE + +```solidity +bytes32 JIV_PAUSE_OPERATOR_ROLE +``` + +actor that can pause JIV + +### _getNameSymbol + +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole + +```solidity +function _minterRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can mint JIV token_ + +### _burnerRole + +```solidity +function _burnerRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can burn JIV token_ + +### _pauserRole + +```solidity +function _pauserRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can pause JIV token_ + +## JivCustomAggregatorFeed + +AggregatorV3 compatible feed for JIV, +where price is submitted manually by feed admins + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## JivDataFeed + +DataFeed for JIV product + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## AcreMBtc1CustomAggregatorFeed + +AggregatorV3 compatible feed for acremBTC1, +where price is submitted manually by feed admins + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## AcreMBtc1DataFeed + +DataFeed for acremBTC1 product + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## acremBTC1 + +### ACRE_BTC_MINT_OPERATOR_ROLE + +```solidity +bytes32 ACRE_BTC_MINT_OPERATOR_ROLE +``` + +actor that can mint acremBTC1 + +### ACRE_BTC_BURN_OPERATOR_ROLE + +```solidity +bytes32 ACRE_BTC_BURN_OPERATOR_ROLE +``` + +actor that can burn acremBTC1 + +### ACRE_BTC_PAUSE_OPERATOR_ROLE + +```solidity +bytes32 ACRE_BTC_PAUSE_OPERATOR_ROLE +``` + +actor that can pause acremBTC1 + +### name + +```solidity +function name() public pure returns (string _name) +``` + +_override to return a new name (not the initial one)_ + +### symbol + +```solidity +function symbol() public pure returns (string _symbol) +``` + +_override to return a new symbol (not the initial one)_ + +### _getNameSymbol + +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole + +```solidity +function _minterRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can mint acremBTC1 token_ + +### _burnerRole + +```solidity +function _burnerRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can burn acremBTC1 token_ + +### _pauserRole + +```solidity +function _pauserRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can pause acremBTC1 token_ + +## CUsdoCustomAggregatorFeed + +AggregatorV3 compatible feed for cUSDO, +where price is submitted manually by feed admins + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## CUsdoDataFeed + +DataFeed for cUSDO product + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## cUSDO + +### C_USDO_MINT_OPERATOR_ROLE + +```solidity +bytes32 C_USDO_MINT_OPERATOR_ROLE +``` + +actor that can mint cUSDO + +### C_USDO_BURN_OPERATOR_ROLE + +```solidity +bytes32 C_USDO_BURN_OPERATOR_ROLE +``` + +actor that can burn cUSDO + +### C_USDO_PAUSE_OPERATOR_ROLE + +```solidity +bytes32 C_USDO_PAUSE_OPERATOR_ROLE +``` + +actor that can pause cUSDO + +### _getNameSymbol + +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole + +```solidity +function _minterRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can mint cUSDO token_ + +### _burnerRole + +```solidity +function _burnerRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can burn cUSDO token_ + +### _pauserRole + +```solidity +function _pauserRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can pause cUSDO token_ + +## DnEthCustomAggregatorFeed + +AggregatorV3 compatible feed for dnETH, +where price is submitted manually by feed admins + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## DnEthDataFeed + +DataFeed for dnETH product + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## dnETH + +### DN_ETH_MINT_OPERATOR_ROLE + +```solidity +bytes32 DN_ETH_MINT_OPERATOR_ROLE +``` + +actor that can mint dnETH + +### DN_ETH_BURN_OPERATOR_ROLE + +```solidity +bytes32 DN_ETH_BURN_OPERATOR_ROLE +``` + +actor that can burn dnETH + +### DN_ETH_PAUSE_OPERATOR_ROLE + +```solidity +bytes32 DN_ETH_PAUSE_OPERATOR_ROLE +``` + +actor that can pause dnETH + +### _getNameSymbol + +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole + +```solidity +function _minterRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can mint dnETH token_ + +### _burnerRole + +```solidity +function _burnerRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can burn dnETH token_ + +### _pauserRole + +```solidity +function _pauserRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can pause dnETH token_ + +## DnFartCustomAggregatorFeed + +AggregatorV3 compatible feed for dnFART, +where price is submitted manually by feed admins + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## DnFartDataFeed + +DataFeed for dnFART product + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## dnFART + +### DN_FART_MINT_OPERATOR_ROLE + +```solidity +bytes32 DN_FART_MINT_OPERATOR_ROLE +``` + +actor that can mint dnFART + +### DN_FART_BURN_OPERATOR_ROLE + +```solidity +bytes32 DN_FART_BURN_OPERATOR_ROLE +``` + +actor that can burn dnFART + +### DN_FART_PAUSE_OPERATOR_ROLE + +```solidity +bytes32 DN_FART_PAUSE_OPERATOR_ROLE +``` + +actor that can pause dnFART + +### _getNameSymbol + +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole + +```solidity +function _minterRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can mint dnFART token_ + +### _burnerRole + +```solidity +function _burnerRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can burn dnFART token_ + +### _pauserRole + +```solidity +function _pauserRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can pause dnFART token_ + +## DnHypeCustomAggregatorFeed + +AggregatorV3 compatible feed for dnHYPE, +where price is submitted manually by feed admins + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## DnHypeDataFeed + +DataFeed for dnHYPE product + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## dnHYPE + +### DN_HYPE_MINT_OPERATOR_ROLE + +```solidity +bytes32 DN_HYPE_MINT_OPERATOR_ROLE +``` + +actor that can mint dnHYPE + +### DN_HYPE_BURN_OPERATOR_ROLE + +```solidity +bytes32 DN_HYPE_BURN_OPERATOR_ROLE +``` + +actor that can burn dnHYPE + +### DN_HYPE_PAUSE_OPERATOR_ROLE + +```solidity +bytes32 DN_HYPE_PAUSE_OPERATOR_ROLE +``` + +actor that can pause dnHYPE + +### _getNameSymbol + +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole + +```solidity +function _minterRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can mint dnHYPE token_ + +### _burnerRole + +```solidity +function _burnerRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can burn dnHYPE token_ + +### _pauserRole + +```solidity +function _pauserRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can pause dnHYPE token_ + +## DnPumpCustomAggregatorFeed + +AggregatorV3 compatible feed for dnPUMP, +where price is submitted manually by feed admins + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## DnPumpDataFeed + +DataFeed for dnPUMP product + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## dnPUMP + +### DN_PUMP_MINT_OPERATOR_ROLE + +```solidity +bytes32 DN_PUMP_MINT_OPERATOR_ROLE +``` + +actor that can mint dnPUMP + +### DN_PUMP_BURN_OPERATOR_ROLE + +```solidity +bytes32 DN_PUMP_BURN_OPERATOR_ROLE +``` + +actor that can burn dnPUMP + +### DN_PUMP_PAUSE_OPERATOR_ROLE + +```solidity +bytes32 DN_PUMP_PAUSE_OPERATOR_ROLE +``` + +actor that can pause dnPUMP + +### _getNameSymbol + +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole + +```solidity +function _minterRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can mint dnPUMP token_ + +### _burnerRole + +```solidity +function _burnerRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can burn dnPUMP token_ + +### _pauserRole + +```solidity +function _pauserRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can pause dnPUMP token_ + +## DnTestCustomAggregatorFeedGrowth + +AggregatorV3 compatible feed for dnTEST, +where price is submitted manually by feed admins, +and growth apr applies to the answer. + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## DnTestDataFeed + +DataFeed for dnTEST product + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## dnTEST + +### DN_TEST_MINT_OPERATOR_ROLE + +```solidity +bytes32 DN_TEST_MINT_OPERATOR_ROLE +``` + +actor that can mint dnTEST + +### DN_TEST_BURN_OPERATOR_ROLE + +```solidity +bytes32 DN_TEST_BURN_OPERATOR_ROLE +``` + +actor that can burn dnTEST + +### DN_TEST_PAUSE_OPERATOR_ROLE + +```solidity +bytes32 DN_TEST_PAUSE_OPERATOR_ROLE +``` + +actor that can pause dnTEST + +### _getNameSymbol + +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole + +```solidity +function _minterRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can mint dnTEST token_ + +### _burnerRole + +```solidity +function _burnerRole() internal pure returns (bytes32) ``` -actor that can manage MRe7CustomAggregatorFeed and MRe7DataFeed +_AC role, owner of which can burn dnTEST token_ -## MRe7RedemptionVaultWithSwapper +### _pauserRole -Smart contract that handles mRE7 redemptions +```solidity +function _pauserRole() internal pure returns (bytes32) +``` -### vaultRole +_AC role, owner of which can pause dnTEST token_ + +## eUSD + +### E_USD_MINT_OPERATOR_ROLE ```solidity -function vaultRole() public pure returns (bytes32) +bytes32 E_USD_MINT_OPERATOR_ROLE ``` -## MRe7SolMidasAccessControlRoles +actor that can mint eUSD -Base contract that stores all roles descriptors for mRE7SOL contracts +### E_USD_BURN_OPERATOR_ROLE -### M_RE7SOL_DEPOSIT_VAULT_ADMIN_ROLE +```solidity +bytes32 E_USD_BURN_OPERATOR_ROLE +``` + +actor that can burn eUSD + +### E_USD_PAUSE_OPERATOR_ROLE ```solidity -bytes32 M_RE7SOL_DEPOSIT_VAULT_ADMIN_ROLE +bytes32 E_USD_PAUSE_OPERATOR_ROLE ``` -actor that can manage MRe7SolDepositVault +actor that can pause eUSD -### M_RE7SOL_REDEMPTION_VAULT_ADMIN_ROLE +### _getNameSymbol ```solidity -bytes32 M_RE7SOL_REDEMPTION_VAULT_ADMIN_ROLE +function _getNameSymbol() internal pure returns (string, string) ``` -actor that can manage MRe7SolRedemptionVault +_returns name and symbol of the token_ -### M_RE7SOL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -bytes32 M_RE7SOL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +function _minterRole() internal pure returns (bytes32) ``` -actor that can manage MRe7SolCustomAggregatorFeed and MRe7SolDataFeed +_AC role, owner of which can mint eUSD token_ -## MRe7SolRedemptionVault +### _burnerRole -Smart contract that handles mRE7SOL redemptions +```solidity +function _burnerRole() internal pure returns (bytes32) +``` -### vaultRole +_AC role, owner of which can burn eUSD token_ + +### _pauserRole ```solidity -function vaultRole() public pure returns (bytes32) +function _pauserRole() internal pure returns (bytes32) ``` -## MSlMidasAccessControlRoles +_AC role, owner of which can pause eUSD token_ -Base contract that stores all roles descriptors for mSL contracts +## HBUsdcCustomAggregatorFeed -### M_SL_DEPOSIT_VAULT_ADMIN_ROLE +AggregatorV3 compatible feed for hbUSDC, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -bytes32 M_SL_DEPOSIT_VAULT_ADMIN_ROLE +function feedAdminRole() public pure returns (bytes32) ``` -actor that can manage MSlDepositVault +_describes a role, owner of which can update prices in this feed_ -### M_SL_REDEMPTION_VAULT_ADMIN_ROLE +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## HBUsdcDataFeed + +DataFeed for hbUSDC product + +### feedAdminRole ```solidity -bytes32 M_SL_REDEMPTION_VAULT_ADMIN_ROLE +function feedAdminRole() public pure returns (bytes32) ``` -actor that can manage MSlRedemptionVault +_describes a role, owner of which can manage this feed_ -### M_SL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## hbUSDC + +### HB_USDC_MINT_OPERATOR_ROLE ```solidity -bytes32 M_SL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +bytes32 HB_USDC_MINT_OPERATOR_ROLE ``` -actor that can manage MSlCustomAggregatorFeed and MSlDataFeed +actor that can mint hbUSDC -## MSlRedemptionVaultWithSwapper +### HB_USDC_BURN_OPERATOR_ROLE -Smart contract that handles mSL redemptions +```solidity +bytes32 HB_USDC_BURN_OPERATOR_ROLE +``` -### vaultRole +actor that can burn hbUSDC + +### HB_USDC_PAUSE_OPERATOR_ROLE ```solidity -function vaultRole() public pure returns (bytes32) +bytes32 HB_USDC_PAUSE_OPERATOR_ROLE ``` -## MevBtcMidasAccessControlRoles +actor that can pause hbUSDC -Base contract that stores all roles descriptors for mevBTC contracts +### _getNameSymbol -### MEV_BTC_DEPOSIT_VAULT_ADMIN_ROLE +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -bytes32 MEV_BTC_DEPOSIT_VAULT_ADMIN_ROLE +function _minterRole() internal pure returns (bytes32) ``` -actor that can manage MevBtcDepositVault +_AC role, owner of which can mint hbUSDC token_ -### MEV_BTC_REDEMPTION_VAULT_ADMIN_ROLE +### _burnerRole ```solidity -bytes32 MEV_BTC_REDEMPTION_VAULT_ADMIN_ROLE +function _burnerRole() internal pure returns (bytes32) ``` -actor that can manage MevBtcRedemptionVault +_AC role, owner of which can burn hbUSDC token_ -### MEV_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +### _pauserRole ```solidity -bytes32 MEV_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +function _pauserRole() internal pure returns (bytes32) ``` -actor that can manage MevBtcCustomAggregatorFeed and MevBtcDataFeed +_AC role, owner of which can pause hbUSDC token_ -## MevBtcRedemptionVaultWithSwapper +## HBUsdtCustomAggregatorFeed -Smart contract that handles mevBTC redemptions +AggregatorV3 compatible feed for hbUSDT, +where price is submitted manually by feed admins -### vaultRole +### feedAdminRole ```solidity -function vaultRole() public pure returns (bytes32) +function feedAdminRole() public pure returns (bytes32) ``` -## TBtcMidasAccessControlRoles +_describes a role, owner of which can update prices in this feed_ -Base contract that stores all roles descriptors for tBTC contracts +#### Return Values -### T_BTC_DEPOSIT_VAULT_ADMIN_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## HBUsdtDataFeed + +DataFeed for hbUSDT product + +### feedAdminRole ```solidity -bytes32 T_BTC_DEPOSIT_VAULT_ADMIN_ROLE +function feedAdminRole() public pure returns (bytes32) ``` -actor that can manage TBtcDepositVault +_describes a role, owner of which can manage this feed_ -### T_BTC_REDEMPTION_VAULT_ADMIN_ROLE +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## hbUSDT + +### HB_USDT_MINT_OPERATOR_ROLE ```solidity -bytes32 T_BTC_REDEMPTION_VAULT_ADMIN_ROLE +bytes32 HB_USDT_MINT_OPERATOR_ROLE ``` -actor that can manage TBtcRedemptionVault +actor that can mint hbUSDT -### T_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +### HB_USDT_BURN_OPERATOR_ROLE ```solidity -bytes32 T_BTC_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +bytes32 HB_USDT_BURN_OPERATOR_ROLE ``` -actor that can manage TBtcCustomAggregatorFeed and TBtcDataFeed +actor that can burn hbUSDT -## TBtcRedemptionVaultWithSwapper +### HB_USDT_PAUSE_OPERATOR_ROLE -Smart contract that handles tBTC redemptions +```solidity +bytes32 HB_USDT_PAUSE_OPERATOR_ROLE +``` -### vaultRole +actor that can pause hbUSDT + +### _getNameSymbol ```solidity -function vaultRole() public pure returns (bytes32) +function _getNameSymbol() internal pure returns (string, string) ``` -## TEthMidasAccessControlRoles +_returns name and symbol of the token_ -Base contract that stores all roles descriptors for tETH contracts +#### Return Values -### T_ETH_DEPOSIT_VAULT_ADMIN_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole + +```solidity +function _minterRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can mint hbUSDT token_ + +### _burnerRole + +```solidity +function _burnerRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can burn hbUSDT token_ + +### _pauserRole + +```solidity +function _pauserRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can pause hbUSDT token_ + +## HBXautCustomAggregatorFeed + +AggregatorV3 compatible feed for hbXAUt, +where price is submitted manually by feed admins + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## HBXautDataFeed + +DataFeed for hbXAUt product + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## hbXAUt + +### HB_XAUT_MINT_OPERATOR_ROLE ```solidity -bytes32 T_ETH_DEPOSIT_VAULT_ADMIN_ROLE +bytes32 HB_XAUT_MINT_OPERATOR_ROLE ``` -actor that can manage TEthDepositVault +actor that can mint hbXAUt -### T_ETH_REDEMPTION_VAULT_ADMIN_ROLE +### HB_XAUT_BURN_OPERATOR_ROLE ```solidity -bytes32 T_ETH_REDEMPTION_VAULT_ADMIN_ROLE +bytes32 HB_XAUT_BURN_OPERATOR_ROLE ``` -actor that can manage TEthRedemptionVault +actor that can burn hbXAUt -### T_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +### HB_XAUT_PAUSE_OPERATOR_ROLE ```solidity -bytes32 T_ETH_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +bytes32 HB_XAUT_PAUSE_OPERATOR_ROLE ``` -actor that can manage TEthCustomAggregatorFeed and TEthDataFeed - -## TEthRedemptionVaultWithSwapper - -Smart contract that handles tETH redemptions +actor that can pause hbXAUt -### vaultRole +### _getNameSymbol ```solidity -function vaultRole() public pure returns (bytes32) +function _getNameSymbol() internal pure returns (string, string) ``` -## TUsdeMidasAccessControlRoles +_returns name and symbol of the token_ -Base contract that stores all roles descriptors for tUSDe contracts +#### Return Values -### T_USDE_DEPOSIT_VAULT_ADMIN_ROLE +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -bytes32 T_USDE_DEPOSIT_VAULT_ADMIN_ROLE +function _minterRole() internal pure returns (bytes32) ``` -actor that can manage TUsdeDepositVault +_AC role, owner of which can mint hbXAUt token_ -### T_USDE_REDEMPTION_VAULT_ADMIN_ROLE +### _burnerRole ```solidity -bytes32 T_USDE_REDEMPTION_VAULT_ADMIN_ROLE +function _burnerRole() internal pure returns (bytes32) ``` -actor that can manage TUsdeRedemptionVault +_AC role, owner of which can burn hbXAUt token_ -### T_USDE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +### _pauserRole ```solidity -bytes32 T_USDE_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +function _pauserRole() internal pure returns (bytes32) ``` -actor that can manage TUsdeCustomAggregatorFeed and TUsdeDataFeed +_AC role, owner of which can pause hbXAUt token_ -## TUsdeRedemptionVaultWithSwapper +## HypeBtcCustomAggregatorFeed -Smart contract that handles tUSDe redemptions +AggregatorV3 compatible feed for hypeBTC, +where price is submitted manually by feed admins -### vaultRole +### feedAdminRole ```solidity -function vaultRole() public pure returns (bytes32) +function feedAdminRole() public pure returns (bytes32) ``` -## RedemptionVaultTest +_describes a role, owner of which can update prices in this feed_ -### _disableInitializers +#### Return Values -```solidity -function _disableInitializers() internal -``` +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -_Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. -Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized -to any version. It is recommended to use this to lock implementation contracts that are designed to be called -through proxies. +## HypeBtcDataFeed -Emits an {Initialized} event the first time it is successfully executed._ +DataFeed for hypeBTC product -### initializeWithoutInitializer +### feedAdminRole ```solidity -function initializeWithoutInitializer(address _ac, struct MTokenInitParams _mTokenInitParams, struct ReceiversInitParams _receiversInitParams, struct InstantInitParams _instantInitParams, address _sanctionsList, uint256 _variationTolerance, uint256 _minAmount, struct FiatRedeptionInitParams _fiatRedemptionInitParams, address _requestRedeemer) external +function feedAdminRole() public pure returns (bytes32) ``` -### setOverrideGetTokenRate +_describes a role, owner of which can manage this feed_ -```solidity -function setOverrideGetTokenRate(bool val) external -``` +#### Return Values -### setGetTokenRateValue +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -function setGetTokenRateValue(uint256 val) external -``` +## hypeBTC -### calcAndValidateRedeemTest +### HYPE_BTC_MINT_OPERATOR_ROLE ```solidity -function calcAndValidateRedeemTest(address user, address tokenOut, uint256 amountMTokenIn, bool isInstant, bool isFiat) external returns (struct RedemptionVault.CalcAndValidateRedeemResult calcResult) +bytes32 HYPE_BTC_MINT_OPERATOR_ROLE ``` -### convertUsdToTokenTest +actor that can mint hypeBTC + +### HYPE_BTC_BURN_OPERATOR_ROLE ```solidity -function convertUsdToTokenTest(uint256 amountUsd, address tokenOut) external returns (uint256 amountToken, uint256 tokenRate) +bytes32 HYPE_BTC_BURN_OPERATOR_ROLE ``` -### convertMTokenToUsdTest +actor that can burn hypeBTC + +### HYPE_BTC_PAUSE_OPERATOR_ROLE ```solidity -function convertMTokenToUsdTest(uint256 amountMToken) external returns (uint256 amountUsd, uint256 mTokenRate) +bytes32 HYPE_BTC_PAUSE_OPERATOR_ROLE ``` -### _getTokenRate +actor that can pause hypeBTC + +### _getNameSymbol ```solidity -function _getTokenRate(address dataFeed, bool stable) internal view returns (uint256) +function _getNameSymbol() internal pure returns (string, string) ``` -_get token rate depends on data feed and stablecoin flag_ +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| dataFeed | address | address of dataFeed from token config | -| stable | bool | is stablecoin | - -## RedemptionVaultWithBUIDLTest +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### _disableInitializers +### _minterRole ```solidity -function _disableInitializers() internal +function _minterRole() internal pure returns (bytes32) ``` -_Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. -Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized -to any version. It is recommended to use this to lock implementation contracts that are designed to be called -through proxies. +_AC role, owner of which can mint hypeBTC token_ -Emits an {Initialized} event the first time it is successfully executed._ +### _burnerRole -## RedemptionVaultWithSwapperTest +```solidity +function _burnerRole() internal pure returns (bytes32) +``` -### _disableInitializers +_AC role, owner of which can burn hypeBTC token_ + +### _pauserRole ```solidity -function _disableInitializers() internal +function _pauserRole() internal pure returns (bytes32) ``` -_Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. -Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized -to any version. It is recommended to use this to lock implementation contracts that are designed to be called -through proxies. +_AC role, owner of which can pause hypeBTC token_ -Emits an {Initialized} event the first time it is successfully executed._ +## HypeEthCustomAggregatorFeed -## RedemptionVaultWithUSTBTest +AggregatorV3 compatible feed for hypeETH, +where price is submitted manually by feed admins -### _disableInitializers +### feedAdminRole ```solidity -function _disableInitializers() internal +function feedAdminRole() public pure returns (bytes32) ``` -_Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. -Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized -to any version. It is recommended to use this to lock implementation contracts that are designed to be called -through proxies. +_describes a role, owner of which can update prices in this feed_ -Emits an {Initialized} event the first time it is successfully executed._ +#### Return Values -### checkAndRedeemUSTB +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## HypeEthDataFeed + +DataFeed for hypeETH product + +### feedAdminRole ```solidity -function checkAndRedeemUSTB(address token, uint256 amount) external +function feedAdminRole() public pure returns (bytes32) ``` -## DepositVault +_describes a role, owner of which can manage this feed_ -Smart contract that handles mTBILL minting +#### Return Values -### CalcAndValidateDepositResult +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -struct CalcAndValidateDepositResult { - uint256 tokenAmountInUsd; - uint256 feeTokenAmount; - uint256 amountTokenWithoutFee; - uint256 mintAmount; - uint256 tokenInRate; - uint256 tokenOutRate; - uint256 tokenDecimals; -} -``` +## hypeETH -### minMTokenAmountForFirstDeposit +### HYPE_ETH_MINT_OPERATOR_ROLE ```solidity -uint256 minMTokenAmountForFirstDeposit +bytes32 HYPE_ETH_MINT_OPERATOR_ROLE ``` -minimal USD amount for first user`s deposit +actor that can mint hypeETH -### mintRequests +### HYPE_ETH_BURN_OPERATOR_ROLE ```solidity -mapping(uint256 => struct Request) mintRequests +bytes32 HYPE_ETH_BURN_OPERATOR_ROLE ``` -mapping, requestId => request data +actor that can burn hypeETH -### totalMinted +### HYPE_ETH_PAUSE_OPERATOR_ROLE ```solidity -mapping(address => uint256) totalMinted +bytes32 HYPE_ETH_PAUSE_OPERATOR_ROLE ``` -_depositor address => amount minted_ +actor that can pause hypeETH -### initialize +### _getNameSymbol ```solidity -function initialize(address _ac, struct MTokenInitParams _mTokenInitParams, struct ReceiversInitParams _receiversInitParams, struct InstantInitParams _instantInitParams, address _sanctionsList, uint256 _variationTolerance, uint256 _minAmount, uint256 _minMTokenAmountForFirstDeposit) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _ac | address | address of MidasAccessControll contract | -| _mTokenInitParams | struct MTokenInitParams | init params for mToken | -| _receiversInitParams | struct ReceiversInitParams | init params for receivers | -| _instantInitParams | struct InstantInitParams | init params for instant operations | -| _sanctionsList | address | address of sanctionsList contract | -| _variationTolerance | uint256 | percent of prices diviation 1% = 100 | -| _minAmount | uint256 | basic min amount for operations in mToken | -| _minMTokenAmountForFirstDeposit | uint256 | min amount for first deposit in mToken | +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### depositInstant +### _minterRole ```solidity -function depositInstant(address tokenIn, uint256 amountToken, uint256 minReceiveAmount, bytes32 referrerId) external +function _minterRole() internal pure returns (bytes32) ``` -depositing proccess with auto mint if -account fit daily limit and token allowance. -Transfers token from the user. -Transfers fee in tokenIn to feeReceiver. -Mints mToken to user. - -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| tokenIn | address | address of tokenIn | -| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | -| minReceiveAmount | uint256 | minimum expected amount of mToken to receive (decimals 18) | -| referrerId | bytes32 | referrer id | +_AC role, owner of which can mint hypeETH token_ -### depositInstant +### _burnerRole ```solidity -function depositInstant(address tokenIn, uint256 amountToken, uint256 minReceiveAmount, bytes32 referrerId, address recipient) external +function _burnerRole() internal pure returns (bytes32) ``` -Does the same as `depositInstant` but allows specifying a custom tokensReceiver address. - -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| tokenIn | address | address of tokenIn | -| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | -| minReceiveAmount | uint256 | minimum expected amount of mToken to receive (decimals 18) | -| referrerId | bytes32 | referrer id | -| recipient | address | | +_AC role, owner of which can burn hypeETH token_ -### depositRequest +### _pauserRole ```solidity -function depositRequest(address tokenIn, uint256 amountToken, bytes32 referrerId) external returns (uint256) +function _pauserRole() internal pure returns (bytes32) ``` -depositing proccess with mint request creating if -account fit token allowance. -Transfers token from the user. -Transfers fee in tokenIn to feeReceiver. -Creates mint request. +_AC role, owner of which can pause hypeETH token_ -#### Parameters +## HypeUsdCustomAggregatorFeed + +AggregatorV3 compatible feed for hypeUSD, +where price is submitted manually by feed admins + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` -| Name | Type | Description | -| ---- | ---- | ----------- | -| tokenIn | address | address of tokenIn | -| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | -| referrerId | bytes32 | referrer id | +_describes a role, owner of which can update prices in this feed_ #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | uint256 | request id | +| [0] | bytes32 | role descriptor | -### depositRequest +## HypeUsdDataFeed + +DataFeed for hypeUSD product + +### feedAdminRole ```solidity -function depositRequest(address tokenIn, uint256 amountToken, bytes32 referrerId, address recipient) external returns (uint256) +function feedAdminRole() public pure returns (bytes32) ``` -Does the same as `depositRequest` but allows specifying a custom tokensReceiver address. +_describes a role, owner of which can manage this feed_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| tokenIn | address | address of tokenIn | -| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | -| referrerId | bytes32 | referrer id | -| recipient | address | address that receives the mTokens | +| [0] | bytes32 | role descriptor | -#### Return Values +## hypeUSD -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | uint256 | request id | +### HYPE_USD_MINT_OPERATOR_ROLE -### safeApproveRequest +```solidity +bytes32 HYPE_USD_MINT_OPERATOR_ROLE +``` + +actor that can mint hypeUSD + +### HYPE_USD_BURN_OPERATOR_ROLE ```solidity -function safeApproveRequest(uint256 requestId, uint256 newOutRate) external +bytes32 HYPE_USD_BURN_OPERATOR_ROLE ``` -approving request if inputted token rate fit price diviation percent -Mints mToken to user. -Sets request flag to Processed. +actor that can burn hypeUSD -#### Parameters +### HYPE_USD_PAUSE_OPERATOR_ROLE -| Name | Type | Description | -| ---- | ---- | ----------- | -| requestId | uint256 | request id | -| newOutRate | uint256 | mToken rate inputted by vault admin | +```solidity +bytes32 HYPE_USD_PAUSE_OPERATOR_ROLE +``` -### approveRequest +actor that can pause hypeUSD + +### _getNameSymbol ```solidity -function approveRequest(uint256 requestId, uint256 newOutRate) external +function _getNameSymbol() internal pure returns (string, string) ``` -approving request without price diviation check -Mints mToken to user. -Sets request flag to Processed. +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| requestId | uint256 | request id | -| newOutRate | uint256 | mToken rate inputted by vault admin | +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### rejectRequest +### _minterRole ```solidity -function rejectRequest(uint256 requestId) external +function _minterRole() internal pure returns (bytes32) ``` -rejecting request -Sets request flag to Canceled. +_AC role, owner of which can mint hypeUSD token_ -#### Parameters +### _burnerRole -| Name | Type | Description | -| ---- | ---- | ----------- | -| requestId | uint256 | request id | +```solidity +function _burnerRole() internal pure returns (bytes32) +``` -### setMinMTokenAmountForFirstDeposit +_AC role, owner of which can burn hypeUSD token_ + +### _pauserRole ```solidity -function setMinMTokenAmountForFirstDeposit(uint256 newValue) external +function _pauserRole() internal pure returns (bytes32) ``` -sets new minimal amount to deposit in EUR. -can be called only from vault`s admin +_AC role, owner of which can pause hypeUSD token_ -#### Parameters +## KitBtcCustomAggregatorFeed -| Name | Type | Description | -| ---- | ---- | ----------- | -| newValue | uint256 | new min. deposit value | +AggregatorV3 compatible feed for kitBTC, +where price is submitted manually by feed admins -### vaultRole +### feedAdminRole ```solidity -function vaultRole() public pure virtual returns (bytes32) +function feedAdminRole() public pure returns (bytes32) ``` -AC role of vault administrator +_describes a role, owner of which can update prices in this feed_ #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | bytes32 | role bytes32 role | +| [0] | bytes32 | role descriptor | -### _validateMinAmount +## KitBtcDataFeed + +DataFeed for kitBTC product + +### feedAdminRole ```solidity -function _validateMinAmount(address user, uint256 amountMTokenWithoutFee) internal view +function feedAdminRole() public pure returns (bytes32) ``` -_validates that inputted USD amount >= minAmountToDepositInUsd() -and amount >= minAmount()_ +_describes a role, owner of which can manage this feed_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| user | address | user address | -| amountMTokenWithoutFee | uint256 | amount of mToken without fee (decimals 18) | +| [0] | bytes32 | role descriptor | -### _depositInstant +## kitBTC + +### KIT_BTC_MINT_OPERATOR_ROLE ```solidity -function _depositInstant(address tokenIn, uint256 amountToken, uint256 minReceiveAmount, address recipient) internal virtual returns (struct DepositVault.CalcAndValidateDepositResult result) +bytes32 KIT_BTC_MINT_OPERATOR_ROLE ``` -_internal deposit instant logic_ - -#### Parameters +actor that can mint kitBTC -| Name | Type | Description | -| ---- | ---- | ----------- | -| tokenIn | address | tokenIn address | -| amountToken | uint256 | amount of tokenIn (decimals 18) | -| minReceiveAmount | uint256 | min amount of mToken to receive (decimals 18) | -| recipient | address | recipient address | +### KIT_BTC_BURN_OPERATOR_ROLE -#### Return Values +```solidity +bytes32 KIT_BTC_BURN_OPERATOR_ROLE +``` -| Name | Type | Description | -| ---- | ---- | ----------- | -| result | struct DepositVault.CalcAndValidateDepositResult | calculated deposit result | +actor that can burn kitBTC -### _calcAndValidateDeposit +### KIT_BTC_PAUSE_OPERATOR_ROLE ```solidity -function _calcAndValidateDeposit(address user, address tokenIn, uint256 amountToken, bool isInstant) internal returns (struct DepositVault.CalcAndValidateDepositResult result) +bytes32 KIT_BTC_PAUSE_OPERATOR_ROLE ``` -_validate deposit and calculate mint amount_ +actor that can pause kitBTC -#### Parameters +### _getNameSymbol -| Name | Type | Description | -| ---- | ---- | ----------- | -| user | address | user address | -| tokenIn | address | tokenIn address | -| amountToken | uint256 | tokenIn amount (decimals 18) | -| isInstant | bool | is instant operation | +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| result | struct DepositVault.CalcAndValidateDepositResult | calculated deposit result | +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### _convertTokenToUsd +### _minterRole ```solidity -function _convertTokenToUsd(address tokenIn, uint256 amount) internal view virtual returns (uint256 amountInUsd, uint256 rate) +function _minterRole() internal pure returns (bytes32) ``` -_calculates USD amount from tokenIn amount_ - -#### Parameters +_AC role, owner of which can mint kitBTC token_ -| Name | Type | Description | -| ---- | ---- | ----------- | -| tokenIn | address | tokenIn address | -| amount | uint256 | amount of tokenIn (decimals 18) | +### _burnerRole -#### Return Values +```solidity +function _burnerRole() internal pure returns (bytes32) +``` -| Name | Type | Description | -| ---- | ---- | ----------- | -| amountInUsd | uint256 | converted amount to USD | -| rate | uint256 | conversion rate | +_AC role, owner of which can burn kitBTC token_ -### _convertUsdToMToken +### _pauserRole ```solidity -function _convertUsdToMToken(uint256 amountUsd) internal view virtual returns (uint256 amountMToken, uint256 mTokenRate) +function _pauserRole() internal pure returns (bytes32) ``` -_calculates mToken amount from USD amount_ +_AC role, owner of which can pause kitBTC token_ -#### Parameters +## KitHypeCustomAggregatorFeed -| Name | Type | Description | -| ---- | ---- | ----------- | -| amountUsd | uint256 | amount of USD (decimals 18) | +AggregatorV3 compatible feed for kitHYPE, +where price is submitted manually by feed admins + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can update prices in this feed_ #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| amountMToken | uint256 | converted USD to mToken | -| mTokenRate | uint256 | conversion rate | - -## EUsdDepositVault - -Smart contract that handles eUSD minting +| [0] | bytes32 | role descriptor | -### vaultRole +## KitHypeDataFeed -```solidity -function vaultRole() public pure returns (bytes32) -``` +DataFeed for kitHYPE product -### greenlistedRole +### feedAdminRole ```solidity -function greenlistedRole() public pure returns (bytes32) +function feedAdminRole() public pure returns (bytes32) ``` -AC role of a greenlist +_describes a role, owner of which can manage this feed_ #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | bytes32 | role bytes32 role | - -## HBUsdtDepositVault +| [0] | bytes32 | role descriptor | -Smart contract that handles hbUSDT minting +## kitHYPE -### vaultRole +### KIT_HYPE_MINT_OPERATOR_ROLE ```solidity -function vaultRole() public pure returns (bytes32) +bytes32 KIT_HYPE_MINT_OPERATOR_ROLE ``` -## HBXautDepositVault - -Smart contract that handles hbXAUt minting +actor that can mint kitHYPE -### vaultRole +### KIT_HYPE_BURN_OPERATOR_ROLE ```solidity -function vaultRole() public pure returns (bytes32) +bytes32 KIT_HYPE_BURN_OPERATOR_ROLE ``` -## HypeBtcDepositVault +actor that can burn kitHYPE -Smart contract that handles hypeBTC minting +### KIT_HYPE_PAUSE_OPERATOR_ROLE -### vaultRole +```solidity +bytes32 KIT_HYPE_PAUSE_OPERATOR_ROLE +``` + +actor that can pause kitHYPE + +### _getNameSymbol ```solidity -function vaultRole() public pure returns (bytes32) +function _getNameSymbol() internal pure returns (string, string) ``` -## HypeEthDepositVault +_returns name and symbol of the token_ -Smart contract that handles hypeETH minting +#### Return Values -### vaultRole +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -function vaultRole() public pure returns (bytes32) +function _minterRole() internal pure returns (bytes32) ``` -## HypeUsdDepositVault - -Smart contract that handles hypeUSD minting +_AC role, owner of which can mint kitHYPE token_ -### vaultRole +### _burnerRole ```solidity -function vaultRole() public pure returns (bytes32) +function _burnerRole() internal pure returns (bytes32) ``` -## Request +_AC role, owner of which can burn kitHYPE token_ + +### _pauserRole ```solidity -struct Request { - address sender; - address tokenIn; - enum RequestStatus status; - uint256 depositedUsdAmount; - uint256 usdAmountWithoutFees; - uint256 tokenOutRate; -} +function _pauserRole() internal pure returns (bytes32) ``` -## IDepositVault +_AC role, owner of which can pause kitHYPE token_ + +## KitUsdCustomAggregatorFeed + +AggregatorV3 compatible feed for kitUSD, +where price is submitted manually by feed admins -### SetMinMTokenAmountForFirstDeposit +### feedAdminRole ```solidity -event SetMinMTokenAmountForFirstDeposit(address caller, uint256 newValue) +function feedAdminRole() public pure returns (bytes32) ``` -#### Parameters +_describes a role, owner of which can update prices in this feed_ + +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| caller | address | function caller (msg.sender) | -| newValue | uint256 | new min amount to deposit value | +| [0] | bytes32 | role descriptor | -### DepositInstant +## KitUsdDataFeed + +DataFeed for kitUSD product + +### feedAdminRole ```solidity -event DepositInstant(address user, address tokenIn, uint256 amountUsd, uint256 amountToken, uint256 fee, uint256 minted, bytes32 referrerId) +function feedAdminRole() public pure returns (bytes32) ``` -#### Parameters +_describes a role, owner of which can manage this feed_ + +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| user | address | function caller (msg.sender) | -| tokenIn | address | address of tokenIn | -| amountUsd | uint256 | amount of tokenIn converted to USD | -| amountToken | uint256 | amount of tokenIn | -| fee | uint256 | fee amount in tokenIn | -| minted | uint256 | amount of minted mTokens | -| referrerId | bytes32 | referrer id | +| [0] | bytes32 | role descriptor | + +## kitUSD -### DepositInstantWithCustomRecipient +### KIT_USD_MINT_OPERATOR_ROLE ```solidity -event DepositInstantWithCustomRecipient(address user, address tokenIn, address recipient, uint256 amountUsd, uint256 amountToken, uint256 fee, uint256 minted, bytes32 referrerId) +bytes32 KIT_USD_MINT_OPERATOR_ROLE ``` -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| user | address | function caller (msg.sender) | -| tokenIn | address | address of tokenIn | -| recipient | address | address that receives the mTokens | -| amountUsd | uint256 | amount of tokenIn converted to USD | -| amountToken | uint256 | amount of tokenIn | -| fee | uint256 | fee amount in tokenIn | -| minted | uint256 | amount of minted mTokens | -| referrerId | bytes32 | referrer id | +actor that can mint kitUSD -### DepositRequest +### KIT_USD_BURN_OPERATOR_ROLE ```solidity -event DepositRequest(uint256 requestId, address user, address tokenIn, uint256 amountToken, uint256 amountUsd, uint256 fee, uint256 tokenOutRate, bytes32 referrerId) +bytes32 KIT_USD_BURN_OPERATOR_ROLE ``` -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| requestId | uint256 | mint request id | -| user | address | function caller (msg.sender) | -| tokenIn | address | address of tokenIn | -| amountToken | uint256 | amount of tokenIn | -| amountUsd | uint256 | amount of tokenIn converted to USD | -| fee | uint256 | fee amount in tokenIn | -| tokenOutRate | uint256 | mToken rate | -| referrerId | bytes32 | referrer id | +actor that can burn kitUSD -### DepositRequestWithCustomRecipient +### KIT_USD_PAUSE_OPERATOR_ROLE ```solidity -event DepositRequestWithCustomRecipient(uint256 requestId, address user, address tokenIn, address recipient, uint256 amountToken, uint256 amountUsd, uint256 fee, uint256 tokenOutRate, bytes32 referrerId) +bytes32 KIT_USD_PAUSE_OPERATOR_ROLE ``` -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| requestId | uint256 | mint request id | -| user | address | function caller (msg.sender) | -| tokenIn | address | address of tokenIn | -| recipient | address | address that receives the mTokens | -| amountToken | uint256 | amount of tokenIn | -| amountUsd | uint256 | amount of tokenIn converted to USD | -| fee | uint256 | fee amount in tokenIn | -| tokenOutRate | uint256 | mToken rate | -| referrerId | bytes32 | referrer id | +actor that can pause kitUSD -### ApproveRequest +### _getNameSymbol ```solidity -event ApproveRequest(uint256 requestId, uint256 newOutRate) +function _getNameSymbol() internal pure returns (string, string) ``` -#### Parameters +_returns name and symbol of the token_ + +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| requestId | uint256 | mint request id | -| newOutRate | uint256 | mToken rate inputted by admin | +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### SafeApproveRequest +### _minterRole ```solidity -event SafeApproveRequest(uint256 requestId, uint256 newOutRate) +function _minterRole() internal pure returns (bytes32) ``` -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| requestId | uint256 | mint request id | -| newOutRate | uint256 | mToken rate inputted by admin | +_AC role, owner of which can mint kitUSD token_ -### RejectRequest +### _burnerRole ```solidity -event RejectRequest(uint256 requestId, address user) +function _burnerRole() internal pure returns (bytes32) ``` -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| requestId | uint256 | mint request id | -| user | address | address of user | +_AC role, owner of which can burn kitUSD token_ -### FreeFromMinDeposit +### _pauserRole ```solidity -event FreeFromMinDeposit(address user) +function _pauserRole() internal pure returns (bytes32) ``` -#### Parameters +_AC role, owner of which can pause kitUSD token_ -| Name | Type | Description | -| ---- | ---- | ----------- | -| user | address | address that was freed from min deposit check | +## KmiUsdCustomAggregatorFeed -### depositInstant +AggregatorV3 compatible feed for kmiUSD, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -function depositInstant(address tokenIn, uint256 amountToken, uint256 minReceiveAmount, bytes32 referrerId) external +function feedAdminRole() public pure returns (bytes32) ``` -depositing proccess with auto mint if -account fit daily limit and token allowance. -Transfers token from the user. -Transfers fee in tokenIn to feeReceiver. -Mints mToken to user. +_describes a role, owner of which can update prices in this feed_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| tokenIn | address | address of tokenIn | -| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | -| minReceiveAmount | uint256 | minimum expected amount of mToken to receive (decimals 18) | -| referrerId | bytes32 | referrer id | +| [0] | bytes32 | role descriptor | -### depositInstant +## KmiUsdDataFeed + +DataFeed for kmiUSD product + +### feedAdminRole ```solidity -function depositInstant(address tokenIn, uint256 amountToken, uint256 minReceiveAmount, bytes32 referrerId, address tokensReceiver) external +function feedAdminRole() public pure returns (bytes32) ``` -Does the same as `depositInstant` but allows specifying a custom tokensReceiver address. +_describes a role, owner of which can manage this feed_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| tokenIn | address | address of tokenIn | -| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | -| minReceiveAmount | uint256 | minimum expected amount of mToken to receive (decimals 18) | -| referrerId | bytes32 | referrer id | -| tokensReceiver | address | address to receive the tokens (instead of msg.sender) | +| [0] | bytes32 | role descriptor | -### depositRequest +## kmiUSD + +### KMI_USD_MINT_OPERATOR_ROLE ```solidity -function depositRequest(address tokenIn, uint256 amountToken, bytes32 referrerId) external returns (uint256) +bytes32 KMI_USD_MINT_OPERATOR_ROLE ``` -depositing proccess with mint request creating if -account fit token allowance. -Transfers token from the user. -Transfers fee in tokenIn to feeReceiver. -Creates mint request. - -#### Parameters +actor that can mint kmiUSD -| Name | Type | Description | -| ---- | ---- | ----------- | -| tokenIn | address | address of tokenIn | -| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | -| referrerId | bytes32 | referrer id | +### KMI_USD_BURN_OPERATOR_ROLE -#### Return Values +```solidity +bytes32 KMI_USD_BURN_OPERATOR_ROLE +``` -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | uint256 | request id | +actor that can burn kmiUSD -### depositRequest +### KMI_USD_PAUSE_OPERATOR_ROLE ```solidity -function depositRequest(address tokenIn, uint256 amountToken, bytes32 referrerId, address recipient) external returns (uint256) +bytes32 KMI_USD_PAUSE_OPERATOR_ROLE ``` -Does the same as `depositRequest` but allows specifying a custom tokensReceiver address. +actor that can pause kmiUSD -#### Parameters +### _getNameSymbol -| Name | Type | Description | -| ---- | ---- | ----------- | -| tokenIn | address | address of tokenIn | -| amountToken | uint256 | amount of `tokenIn` that will be taken from user (decimals 18) | -| referrerId | bytes32 | referrer id | -| recipient | address | address that receives the mTokens | +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | uint256 | request id | +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### safeApproveRequest +### _minterRole ```solidity -function safeApproveRequest(uint256 requestId, uint256 newOutRate) external +function _minterRole() internal pure returns (bytes32) ``` -approving request if inputted token rate fit price diviation percent -Mints mToken to user. -Sets request flag to Processed. +_AC role, owner of which can mint kmiUSD token_ -#### Parameters +### _burnerRole -| Name | Type | Description | -| ---- | ---- | ----------- | -| requestId | uint256 | request id | -| newOutRate | uint256 | mToken rate inputted by vault admin | +```solidity +function _burnerRole() internal pure returns (bytes32) +``` -### approveRequest +_AC role, owner of which can burn kmiUSD token_ + +### _pauserRole ```solidity -function approveRequest(uint256 requestId, uint256 newOutRate) external +function _pauserRole() internal pure returns (bytes32) ``` -approving request without price diviation check -Mints mToken to user. -Sets request flag to Processed. +_AC role, owner of which can pause kmiUSD token_ -#### Parameters +## LiquidHypeCustomAggregatorFeed -| Name | Type | Description | -| ---- | ---- | ----------- | -| requestId | uint256 | request id | -| newOutRate | uint256 | mToken rate inputted by vault admin | +AggregatorV3 compatible feed for liquidHYPE, +where price is submitted manually by feed admins -### rejectRequest +### feedAdminRole ```solidity -function rejectRequest(uint256 requestId) external +function feedAdminRole() public pure returns (bytes32) ``` -rejecting request -Sets request flag to Canceled. +_describes a role, owner of which can update prices in this feed_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| requestId | uint256 | request id | +| [0] | bytes32 | role descriptor | -### setMinMTokenAmountForFirstDeposit +## LiquidHypeDataFeed + +DataFeed for liquidHYPE product + +### feedAdminRole ```solidity -function setMinMTokenAmountForFirstDeposit(uint256 newValue) external +function feedAdminRole() public pure returns (bytes32) ``` -sets new minimal amount to deposit in EUR. -can be called only from vault`s admin +_describes a role, owner of which can manage this feed_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| newValue | uint256 | new min. deposit value | - -## MBtcDepositVault +| [0] | bytes32 | role descriptor | -Smart contract that handles mBTC minting +## liquidHYPE -### vaultRole +### LIQUID_HYPE_MINT_OPERATOR_ROLE ```solidity -function vaultRole() public pure returns (bytes32) +bytes32 LIQUID_HYPE_MINT_OPERATOR_ROLE ``` -## TACmBtcDepositVault - -Smart contract that handles TACmBTC minting +actor that can mint liquidHYPE -### vaultRole +### LIQUID_HYPE_BURN_OPERATOR_ROLE ```solidity -function vaultRole() public pure returns (bytes32) +bytes32 LIQUID_HYPE_BURN_OPERATOR_ROLE ``` -## MBasisDepositVault - -Smart contract that handles mBASIS minting +actor that can burn liquidHYPE -### vaultRole +### LIQUID_HYPE_PAUSE_OPERATOR_ROLE ```solidity -function vaultRole() public pure returns (bytes32) +bytes32 LIQUID_HYPE_PAUSE_OPERATOR_ROLE ``` -## MEdgeDepositVault - -Smart contract that handles mEDGE minting +actor that can pause liquidHYPE -### vaultRole +### _getNameSymbol ```solidity -function vaultRole() public pure returns (bytes32) +function _getNameSymbol() internal pure returns (string, string) ``` -## TACmEdgeDepositVault +_returns name and symbol of the token_ -Smart contract that handles TACmEdge minting +#### Return Values -### vaultRole +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -function vaultRole() public pure returns (bytes32) +function _minterRole() internal pure returns (bytes32) ``` -## MFOneDepositVault - -Smart contract that handles mF-ONE minting +_AC role, owner of which can mint liquidHYPE token_ -### vaultRole +### _burnerRole ```solidity -function vaultRole() public pure returns (bytes32) +function _burnerRole() internal pure returns (bytes32) ``` -## MLiquidityDepositVault - -Smart contract that handles mLIQUIDITY minting +_AC role, owner of which can burn liquidHYPE token_ -### vaultRole +### _pauserRole ```solidity -function vaultRole() public pure returns (bytes32) +function _pauserRole() internal pure returns (bytes32) ``` -## MMevDepositVault +_AC role, owner of which can pause liquidHYPE token_ -Smart contract that handles mMEV minting +## LiquidReserveCustomAggregatorFeed -### vaultRole +AggregatorV3 compatible feed for liquidRESERVE, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -function vaultRole() public pure returns (bytes32) +function feedAdminRole() public pure returns (bytes32) ``` -## TACmMevDepositVault - -Smart contract that handles TACmMEV minting +_describes a role, owner of which can update prices in this feed_ -### vaultRole +#### Return Values -```solidity -function vaultRole() public pure returns (bytes32) -``` +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -## MRe7DepositVault +## LiquidReserveDataFeed -Smart contract that handles mRE7 minting +DataFeed for liquidRESERVE product -### vaultRole +### feedAdminRole ```solidity -function vaultRole() public pure returns (bytes32) +function feedAdminRole() public pure returns (bytes32) ``` -## MRe7SolDepositVault +_describes a role, owner of which can manage this feed_ -Smart contract that handles mRE7SOL minting +#### Return Values -### vaultRole +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## liquidRESERVE + +### LIQUID_RESERVE_MINT_OPERATOR_ROLE ```solidity -function vaultRole() public pure returns (bytes32) +bytes32 LIQUID_RESERVE_MINT_OPERATOR_ROLE ``` -## MSlDepositVault - -Smart contract that handles mSL minting +actor that can mint liquidRESERVE -### vaultRole +### LIQUID_RESERVE_BURN_OPERATOR_ROLE ```solidity -function vaultRole() public pure returns (bytes32) +bytes32 LIQUID_RESERVE_BURN_OPERATOR_ROLE ``` -## MevBtcDepositVault - -Smart contract that handles mevBTC minting +actor that can burn liquidRESERVE -### vaultRole +### LIQUID_RESERVE_PAUSE_OPERATOR_ROLE ```solidity -function vaultRole() public pure returns (bytes32) +bytes32 LIQUID_RESERVE_PAUSE_OPERATOR_ROLE ``` -## TBtcDepositVault - -Smart contract that handles tBTC minting +actor that can pause liquidRESERVE -### vaultRole +### _getNameSymbol ```solidity -function vaultRole() public pure returns (bytes32) +function _getNameSymbol() internal pure returns (string, string) ``` -## TEthDepositVault +_returns name and symbol of the token_ -Smart contract that handles tETH minting +#### Return Values -### vaultRole +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -function vaultRole() public pure returns (bytes32) +function _minterRole() internal pure returns (bytes32) ``` -## TUsdeDepositVault - -Smart contract that handles tUSDe minting +_AC role, owner of which can mint liquidRESERVE token_ -### vaultRole +### _burnerRole ```solidity -function vaultRole() public pure returns (bytes32) +function _burnerRole() internal pure returns (bytes32) ``` -## DepositVaultTest +_AC role, owner of which can burn liquidRESERVE token_ -### _disableInitializers +### _pauserRole ```solidity -function _disableInitializers() internal +function _pauserRole() internal pure returns (bytes32) ``` -_Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. -Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized -to any version. It is recommended to use this to lock implementation contracts that are designed to be called -through proxies. +_AC role, owner of which can pause liquidRESERVE token_ -Emits an {Initialized} event the first time it is successfully executed._ +## LstHypeCustomAggregatorFeed -### tokenTransferFromToTester +AggregatorV3 compatible feed for lstHYPE, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -function tokenTransferFromToTester(address token, address from, address to, uint256 amount, uint256 tokenDecimals) external +function feedAdminRole() public pure returns (bytes32) ``` -### tokenTransferToUserTester +_describes a role, owner of which can update prices in this feed_ -```solidity -function tokenTransferToUserTester(address token, address to, uint256 amount, uint256 tokenDecimals) external -``` +#### Return Values -### setOverrideGetTokenRate +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -function setOverrideGetTokenRate(bool val) external -``` +## LstHypeDataFeed -### setGetTokenRateValue +DataFeed for lstHYPE product + +### feedAdminRole ```solidity -function setGetTokenRateValue(uint256 val) external +function feedAdminRole() public pure returns (bytes32) ``` -### calcAndValidateDeposit +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## lstHYPE + +### LST_HYPE_MINT_OPERATOR_ROLE ```solidity -function calcAndValidateDeposit(address user, address tokenIn, uint256 amountToken, bool isInstant) external returns (struct DepositVault.CalcAndValidateDepositResult) +bytes32 LST_HYPE_MINT_OPERATOR_ROLE ``` -### convertTokenToUsdTest +actor that can mint lstHYPE + +### LST_HYPE_BURN_OPERATOR_ROLE ```solidity -function convertTokenToUsdTest(address tokenIn, uint256 amount) external returns (uint256 amountInUsd, uint256 rate) +bytes32 LST_HYPE_BURN_OPERATOR_ROLE ``` -### convertUsdToMTokenTest +actor that can burn lstHYPE + +### LST_HYPE_PAUSE_OPERATOR_ROLE ```solidity -function convertUsdToMTokenTest(uint256 amountUsd) external returns (uint256 amountMToken, uint256 mTokenRate) +bytes32 LST_HYPE_PAUSE_OPERATOR_ROLE ``` -### _getTokenRate +actor that can pause lstHYPE + +### _getNameSymbol ```solidity -function _getTokenRate(address dataFeed, bool stable) internal view returns (uint256) +function _getNameSymbol() internal pure returns (string, string) ``` -_get token rate depends on data feed and stablecoin flag_ +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| dataFeed | address | address of dataFeed from token config | -| stable | bool | is stablecoin | - -## ManageableVaultTester +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### _disableInitializers +### _minterRole ```solidity -function _disableInitializers() internal +function _minterRole() internal pure returns (bytes32) ``` -_Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. -Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized -to any version. It is recommended to use this to lock implementation contracts that are designed to be called -through proxies. - -Emits an {Initialized} event the first time it is successfully executed._ +_AC role, owner of which can mint lstHYPE token_ -### initialize +### _burnerRole ```solidity -function initialize(address _ac, struct MTokenInitParams _mTokenInitParams, struct ReceiversInitParams _receiversInitParams, struct InstantInitParams _instantInitParams, address _sanctionsList, uint256 _variationTolerance, uint256 _minAmount) external +function _burnerRole() internal pure returns (bytes32) ``` -### initializeWithoutInitializer +_AC role, owner of which can burn lstHYPE token_ + +### _pauserRole ```solidity -function initializeWithoutInitializer(address _ac, struct MTokenInitParams _mTokenInitParams, struct ReceiversInitParams _receiversInitParams, struct InstantInitParams _instantInitParams, address _sanctionsList, uint256 _variationTolerance, uint256 _minAmount) external +function _pauserRole() internal pure returns (bytes32) ``` -### vaultRole +_AC role, owner of which can pause lstHYPE token_ + +## MApolloCustomAggregatorFeed + +AggregatorV3 compatible feed for mAPOLLO, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -function vaultRole() public view virtual returns (bytes32) +function feedAdminRole() public pure returns (bytes32) ``` -AC role of vault administrator +_describes a role, owner of which can update prices in this feed_ #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | bytes32 | role bytes32 role | +| [0] | bytes32 | role descriptor | -## SanctionsListMock +## MApolloDataFeed -### isSanctioned +DataFeed for mAPOLLO product + +### feedAdminRole ```solidity -mapping(address => bool) isSanctioned +function feedAdminRole() public pure returns (bytes32) ``` -### setSanctioned +_describes a role, owner of which can manage this feed_ -```solidity -function setSanctioned(address addr, bool sanctioned) external -``` +#### Return Values -## eUSD +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -### E_USD_MINT_OPERATOR_ROLE +## mAPOLLO + +### M_APOLLO_MINT_OPERATOR_ROLE ```solidity -bytes32 E_USD_MINT_OPERATOR_ROLE +bytes32 M_APOLLO_MINT_OPERATOR_ROLE ``` -actor that can mint eUSD +actor that can mint mAPOLLO -### E_USD_BURN_OPERATOR_ROLE +### M_APOLLO_BURN_OPERATOR_ROLE ```solidity -bytes32 E_USD_BURN_OPERATOR_ROLE +bytes32 M_APOLLO_BURN_OPERATOR_ROLE ``` -actor that can burn eUSD +actor that can burn mAPOLLO -### E_USD_PAUSE_OPERATOR_ROLE +### M_APOLLO_PAUSE_OPERATOR_ROLE ```solidity -bytes32 E_USD_PAUSE_OPERATOR_ROLE +bytes32 M_APOLLO_PAUSE_OPERATOR_ROLE ``` -actor that can pause eUSD +actor that can pause mAPOLLO -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -5349,7 +17972,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint eUSD token_ +_AC role, owner of which can mint mAPOLLO token_ ### _burnerRole @@ -5357,7 +17980,7 @@ _AC role, owner of which can mint eUSD token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn eUSD token_ +_AC role, owner of which can burn mAPOLLO token_ ### _pauserRole @@ -5365,437 +17988,447 @@ _AC role, owner of which can burn eUSD token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause eUSD token_ +_AC role, owner of which can pause mAPOLLO token_ -## CustomAggregatorV3CompatibleFeed +## MBasisCustomAggregatorFeed -AggregatorV3 compatible feed, where price is submitted manually by feed admins +AggregatorV3 compatible feed for mBASIS, +where price is submitted manually by feed admins -### RoundData +### feedAdminRole ```solidity -struct RoundData { - uint80 roundId; - int256 answer; - uint256 startedAt; - uint256 updatedAt; - uint80 answeredInRound; -} +function feedAdminRole() public pure returns (bytes32) ``` -### description - -```solidity -string description -``` +_describes a role, owner of which can update prices in this feed_ -feed description +#### Return Values -### latestRound +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -uint80 latestRound -``` +## MBasisDataFeed -last round id +DataFeed for mBASIS product -### maxAnswerDeviation +### feedAdminRole ```solidity -uint256 maxAnswerDeviation +function feedAdminRole() public pure returns (bytes32) ``` -max deviation from lattest price in % - -_10 ** decimals() is a percentage precision_ +_describes a role, owner of which can manage this feed_ -### minAnswer +#### Return Values -```solidity -int192 minAnswer -``` +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -minimal possible answer that feed can return +## mBASIS -### maxAnswer +### M_BASIS_MINT_OPERATOR_ROLE ```solidity -int192 maxAnswer +bytes32 M_BASIS_MINT_OPERATOR_ROLE ``` -maximal possible answer that feed can return +actor that can mint mBASIS -### AnswerUpdated +### M_BASIS_BURN_OPERATOR_ROLE ```solidity -event AnswerUpdated(int256 data, uint256 roundId, uint256 timestamp) +bytes32 M_BASIS_BURN_OPERATOR_ROLE ``` -### onlyAggregatorAdmin +actor that can burn mBASIS + +### M_BASIS_PAUSE_OPERATOR_ROLE ```solidity -modifier onlyAggregatorAdmin() +bytes32 M_BASIS_PAUSE_OPERATOR_ROLE ``` -_checks that msg.sender do have a feedAdminRole() role_ +actor that can pause mBASIS -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl, int192 _minAnswer, int192 _maxAnswer, uint256 _maxAnswerDeviation, string _description) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | -| _minAnswer | int192 | init value for `minAnswer`. Should be < `_maxAnswer` | -| _maxAnswer | int192 | init value for `maxAnswer`. Should be > `_minAnswer` | -| _maxAnswerDeviation | uint256 | init value for `maxAnswerDeviation` | -| _description | string | init value for `description` | +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### setRoundDataSafe +### _minterRole ```solidity -function setRoundDataSafe(int256 _data) external +function _minterRole() internal pure returns (bytes32) ``` -works as `setRoundData()`, but also checks the -deviation with the lattest submitted data +_AC role, owner of which can mint mBASIS token_ -_deviation with previous data needs to be <= `maxAnswerDeviation`_ +### _burnerRole -#### Parameters +```solidity +function _burnerRole() internal pure returns (bytes32) +``` -| Name | Type | Description | -| ---- | ---- | ----------- | -| _data | int256 | data value | +_AC role, owner of which can burn mBASIS token_ -### setRoundData +### _pauserRole ```solidity -function setRoundData(int256 _data) public +function _pauserRole() internal pure returns (bytes32) ``` -sets the data for `latestRound` + 1 round id - -_`_data` should be >= `minAnswer` and <= `maxAnswer`. -Function should be called only from address with `feedAdminRole()`_ +_AC role, owner of which can pause mBASIS token_ -#### Parameters +## MBtcCustomAggregatorFeed -| Name | Type | Description | -| ---- | ---- | ----------- | -| _data | int256 | data value | +AggregatorV3 compatible feed for mBTC, +where price is submitted manually by feed admins -### latestRoundData +### feedAdminRole ```solidity -function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +function feedAdminRole() public pure returns (bytes32) ``` -### version +_describes a role, owner of which can update prices in this feed_ -```solidity -function version() external pure returns (uint256) -``` +#### Return Values -### lastAnswer +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## MBtcDataFeed + +DataFeed for mBTC product + +### feedAdminRole ```solidity -function lastAnswer() public view returns (int256) +function feedAdminRole() public pure returns (bytes32) ``` +_describes a role, owner of which can manage this feed_ + #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | int256 | answer of lattest price submission | +| [0] | bytes32 | role descriptor | -### lastTimestamp +## mBTC + +### M_BTC_MINT_OPERATOR_ROLE ```solidity -function lastTimestamp() public view returns (uint256) +bytes32 M_BTC_MINT_OPERATOR_ROLE ``` -#### Return Values +actor that can mint mBTC -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | uint256 | timestamp of lattest price submission | +### M_BTC_BURN_OPERATOR_ROLE -### getRoundData +```solidity +bytes32 M_BTC_BURN_OPERATOR_ROLE +``` + +actor that can burn mBTC + +### M_BTC_PAUSE_OPERATOR_ROLE ```solidity -function getRoundData(uint80 _roundId) public view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +bytes32 M_BTC_PAUSE_OPERATOR_ROLE ``` -### feedAdminRole +actor that can pause mBTC + +### _getNameSymbol ```solidity -function feedAdminRole() public view virtual returns (bytes32) +function _getNameSymbol() internal pure returns (string, string) ``` -_describes a role, owner of which can update prices in this feed_ +_returns name and symbol of the token_ #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | bytes32 | role descriptor | +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### decimals +### _minterRole ```solidity -function decimals() public pure returns (uint8) +function _minterRole() internal pure returns (bytes32) ``` -### _getDeviation +_AC role, owner of which can mint mBTC token_ + +### _burnerRole ```solidity -function _getDeviation(int256 _lastPrice, int256 _newPrice) internal pure returns (uint256) +function _burnerRole() internal pure returns (bytes32) ``` -_calculates a deviation in % between `_lastPrice` and `_newPrice`_ +_AC role, owner of which can burn mBTC token_ -#### Return Values +### _pauserRole -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | uint256 | deviation in `10 ** decimals()` precision | +```solidity +function _pauserRole() internal pure returns (bytes32) +``` -## CustomAggregatorV3CompatibleFeedDiscounted +_AC role, owner of which can pause mBTC token_ -AggregatorV3 compatible proxy-feed that discounts the price -of an underlying chainlink compatible feed by a given percentage +## TACmBTC -### underlyingFeed +### TAC_M_BTC_MINT_OPERATOR_ROLE ```solidity -contract AggregatorV3Interface underlyingFeed +bytes32 TAC_M_BTC_MINT_OPERATOR_ROLE ``` -the underlying chainlink compatible feed +actor that can mint TACmBTC -### discountPercentage +### TAC_M_BTC_BURN_OPERATOR_ROLE ```solidity -uint256 discountPercentage +bytes32 TAC_M_BTC_BURN_OPERATOR_ROLE ``` -the discount percentage. Expressed in 10 ** decimals() precision -Example: 10 ** decimals() = 1% +actor that can burn TACmBTC -### constructor +### TAC_M_BTC_PAUSE_OPERATOR_ROLE ```solidity -constructor(address _underlyingFeed, uint256 _discountPercentage) public +bytes32 TAC_M_BTC_PAUSE_OPERATOR_ROLE ``` -constructor +actor that can pause TACmBTC -#### Parameters +### _getNameSymbol + +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _underlyingFeed | address | the underlying chainlink compatible feed | -| _discountPercentage | uint256 | the discount percentage. Expressed in 10 ** decimals() precision | +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### latestRoundData +### _minterRole ```solidity -function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +function _minterRole() internal pure returns (bytes32) ``` -### version - -```solidity -function version() external view returns (uint256) -``` +_AC role, owner of which can mint TACmBTC token_ -### getRoundData +### _burnerRole ```solidity -function getRoundData(uint80 _roundId) public view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +function _burnerRole() internal pure returns (bytes32) ``` -### decimals +_AC role, owner of which can burn TACmBTC token_ + +### _pauserRole ```solidity -function decimals() public view returns (uint8) +function _pauserRole() internal pure returns (bytes32) ``` -### description +_AC role, owner of which can pause TACmBTC token_ -```solidity -function description() public view returns (string) -``` +## MEdgeCustomAggregatorFeed -### _calculateDiscountedAnswer +AggregatorV3 compatible feed for mEDGE, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -function _calculateDiscountedAnswer(int256 _answer) internal view returns (int256) +function feedAdminRole() public pure returns (bytes32) ``` -_calculates the discounted answer_ - -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| _answer | int256 | the answer to discount | +_describes a role, owner of which can update prices in this feed_ #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | int256 | the discounted answer | +| [0] | bytes32 | role descriptor | -## DataFeed +## MEdgeDataFeed -Wrapper of ChainLink`s AggregatorV3 data feeds +DataFeed for mEDGE product -### aggregator +### feedAdminRole ```solidity -contract AggregatorV3Interface aggregator +function feedAdminRole() public pure returns (bytes32) ``` -AggregatorV3Interface contract address +_describes a role, owner of which can manage this feed_ -### healthyDiff +#### Return Values -```solidity -uint256 healthyDiff -``` +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -_healty difference between `block.timestamp` and `updatedAt` timestamps_ +## mEDGE -### minExpectedAnswer +### M_EDGE_MINT_OPERATOR_ROLE ```solidity -int256 minExpectedAnswer +bytes32 M_EDGE_MINT_OPERATOR_ROLE ``` -_minimal answer expected to receive from the `aggregator`_ +actor that can mint mEDGE -### maxExpectedAnswer +### M_EDGE_BURN_OPERATOR_ROLE ```solidity -int256 maxExpectedAnswer +bytes32 M_EDGE_BURN_OPERATOR_ROLE ``` -_maximal answer expected to receive from the `aggregator`_ +actor that can burn mEDGE -### initialize +### M_EDGE_PAUSE_OPERATOR_ROLE ```solidity -function initialize(address _ac, address _aggregator, uint256 _healthyDiff, int256 _minExpectedAnswer, int256 _maxExpectedAnswer) external +bytes32 M_EDGE_PAUSE_OPERATOR_ROLE ``` -upgradeable pattern contract`s initializer - -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| _ac | address | MidasAccessControl contract address | -| _aggregator | address | AggregatorV3Interface contract address | -| _healthyDiff | uint256 | max. staleness time for data feed answers | -| _minExpectedAnswer | int256 | min.expected answer value from data feed | -| _maxExpectedAnswer | int256 | max.expected answer value from data feed | +actor that can pause mEDGE -### changeAggregator +### _getNameSymbol ```solidity -function changeAggregator(address _aggregator) external +function _getNameSymbol() internal pure returns (string, string) ``` -updates `aggregator` address +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _aggregator | address | new AggregatorV3Interface contract address | +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### setHealthyDiff +### _minterRole ```solidity -function setHealthyDiff(uint256 _healthyDiff) external +function _minterRole() internal pure returns (bytes32) ``` -_updates `healthyDiff` value_ +_AC role, owner of which can mint mEDGE token_ -#### Parameters +### _burnerRole -| Name | Type | Description | -| ---- | ---- | ----------- | -| _healthyDiff | uint256 | new value | +```solidity +function _burnerRole() internal pure returns (bytes32) +``` -### setMinExpectedAnswer +_AC role, owner of which can burn mEDGE token_ + +### _pauserRole ```solidity -function setMinExpectedAnswer(int256 _minExpectedAnswer) external +function _pauserRole() internal pure returns (bytes32) ``` -_updates `minExpectedAnswer` value_ +_AC role, owner of which can pause mEDGE token_ -#### Parameters +## TACmEDGE -| Name | Type | Description | -| ---- | ---- | ----------- | -| _minExpectedAnswer | int256 | min value | +### TAC_M_EDGE_MINT_OPERATOR_ROLE -### setMaxExpectedAnswer +```solidity +bytes32 TAC_M_EDGE_MINT_OPERATOR_ROLE +``` + +actor that can mint TACmEDGE + +### TAC_M_EDGE_BURN_OPERATOR_ROLE ```solidity -function setMaxExpectedAnswer(int256 _maxExpectedAnswer) external +bytes32 TAC_M_EDGE_BURN_OPERATOR_ROLE ``` -_updates `maxExpectedAnswer` value_ +actor that can burn TACmEDGE -#### Parameters +### TAC_M_EDGE_PAUSE_OPERATOR_ROLE -| Name | Type | Description | -| ---- | ---- | ----------- | -| _maxExpectedAnswer | int256 | max value | +```solidity +bytes32 TAC_M_EDGE_PAUSE_OPERATOR_ROLE +``` -### getDataInBase18 +actor that can pause TACmEDGE + +### _getNameSymbol ```solidity -function getDataInBase18() external view returns (uint256 answer) +function _getNameSymbol() internal pure returns (string, string) ``` -fetches answer from aggregator -and converts it to the base18 precision +_returns name and symbol of the token_ #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| answer | uint256 | fetched aggregator answer | +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### feedAdminRole +### _minterRole ```solidity -function feedAdminRole() public pure virtual returns (bytes32) +function _minterRole() internal pure returns (bytes32) ``` -_describes a role, owner of which can manage this feed_ +_AC role, owner of which can mint TACmEDGE token_ -#### Return Values +### _burnerRole -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | bytes32 | role descriptor | +```solidity +function _burnerRole() internal pure returns (bytes32) +``` -## HBUsdtCustomAggregatorFeed +_AC role, owner of which can burn TACmEDGE token_ -AggregatorV3 compatible feed for hbUSDT, +### _pauserRole + +```solidity +function _pauserRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can pause TACmEDGE token_ + +## MEvUsdCustomAggregatorFeed + +AggregatorV3 compatible feed for mEVUSD, where price is submitted manually by feed admins ### feedAdminRole @@ -5812,9 +18445,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## HBUsdtDataFeed +## MEvUsdDataFeed -DataFeed for hbUSDT product +DataFeed for mEVUSD product ### feedAdminRole @@ -5830,45 +18463,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## hbUSDT +## mEVUSD -### HB_USDT_MINT_OPERATOR_ROLE +### M_EV_USD_MINT_OPERATOR_ROLE ```solidity -bytes32 HB_USDT_MINT_OPERATOR_ROLE +bytes32 M_EV_USD_MINT_OPERATOR_ROLE ``` -actor that can mint hbUSDT +actor that can mint mEVUSD -### HB_USDT_BURN_OPERATOR_ROLE +### M_EV_USD_BURN_OPERATOR_ROLE ```solidity -bytes32 HB_USDT_BURN_OPERATOR_ROLE +bytes32 M_EV_USD_BURN_OPERATOR_ROLE ``` -actor that can burn hbUSDT +actor that can burn mEVUSD -### HB_USDT_PAUSE_OPERATOR_ROLE +### M_EV_USD_PAUSE_OPERATOR_ROLE ```solidity -bytes32 HB_USDT_PAUSE_OPERATOR_ROLE +bytes32 M_EV_USD_PAUSE_OPERATOR_ROLE ``` -actor that can pause hbUSDT +actor that can pause mEVUSD -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -5876,7 +18510,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint hbUSDT token_ +_AC role, owner of which can mint mEVUSD token_ ### _burnerRole @@ -5884,7 +18518,7 @@ _AC role, owner of which can mint hbUSDT token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn hbUSDT token_ +_AC role, owner of which can burn mEVUSD token_ ### _pauserRole @@ -5892,11 +18526,11 @@ _AC role, owner of which can burn hbUSDT token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause hbUSDT token_ +_AC role, owner of which can pause mEVUSD token_ -## HBXautCustomAggregatorFeed +## MFarmCustomAggregatorFeed -AggregatorV3 compatible feed for hbXAUt, +AggregatorV3 compatible feed for mFARM, where price is submitted manually by feed admins ### feedAdminRole @@ -5913,9 +18547,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## HBXautDataFeed +## MFarmDataFeed -DataFeed for hbXAUt product +DataFeed for mFARM product ### feedAdminRole @@ -5931,45 +18565,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## hbXAUt +## mFARM -### HB_XAUT_MINT_OPERATOR_ROLE +### M_FARM_MINT_OPERATOR_ROLE ```solidity -bytes32 HB_XAUT_MINT_OPERATOR_ROLE +bytes32 M_FARM_MINT_OPERATOR_ROLE ``` -actor that can mint hbXAUt +actor that can mint mFARM -### HB_XAUT_BURN_OPERATOR_ROLE +### M_FARM_BURN_OPERATOR_ROLE ```solidity -bytes32 HB_XAUT_BURN_OPERATOR_ROLE +bytes32 M_FARM_BURN_OPERATOR_ROLE ``` -actor that can burn hbXAUt +actor that can burn mFARM -### HB_XAUT_PAUSE_OPERATOR_ROLE +### M_FARM_PAUSE_OPERATOR_ROLE ```solidity -bytes32 HB_XAUT_PAUSE_OPERATOR_ROLE +bytes32 M_FARM_PAUSE_OPERATOR_ROLE ``` -actor that can pause hbXAUt +actor that can pause mFARM -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -5977,7 +18612,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint hbXAUt token_ +_AC role, owner of which can mint mFARM token_ ### _burnerRole @@ -5985,7 +18620,7 @@ _AC role, owner of which can mint hbXAUt token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn hbXAUt token_ +_AC role, owner of which can burn mFARM token_ ### _pauserRole @@ -5993,11 +18628,11 @@ _AC role, owner of which can burn hbXAUt token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause hbXAUt token_ +_AC role, owner of which can pause mFARM token_ -## HypeBtcCustomAggregatorFeed +## MFOneCustomAggregatorFeed -AggregatorV3 compatible feed for hypeBTC, +AggregatorV3 compatible feed for mF-ONE, where price is submitted manually by feed admins ### feedAdminRole @@ -6014,9 +18649,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## HypeBtcDataFeed +## MFOneDataFeed -DataFeed for hypeBTC product +DataFeed for mF-ONE product ### feedAdminRole @@ -6032,45 +18667,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## hypeBTC +## mFONE -### HYPE_BTC_MINT_OPERATOR_ROLE +### M_FONE_MINT_OPERATOR_ROLE ```solidity -bytes32 HYPE_BTC_MINT_OPERATOR_ROLE +bytes32 M_FONE_MINT_OPERATOR_ROLE ``` -actor that can mint hypeBTC +actor that can mint mF-ONE -### HYPE_BTC_BURN_OPERATOR_ROLE +### M_FONE_BURN_OPERATOR_ROLE ```solidity -bytes32 HYPE_BTC_BURN_OPERATOR_ROLE +bytes32 M_FONE_BURN_OPERATOR_ROLE ``` -actor that can burn hypeBTC +actor that can burn mF-ONE -### HYPE_BTC_PAUSE_OPERATOR_ROLE +### M_FONE_PAUSE_OPERATOR_ROLE ```solidity -bytes32 HYPE_BTC_PAUSE_OPERATOR_ROLE +bytes32 M_FONE_PAUSE_OPERATOR_ROLE ``` -actor that can pause hypeBTC +actor that can pause mF-ONE -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControl contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -6078,7 +18714,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint hypeBTC token_ +_AC role, owner of which can mint mF-ONE token_ ### _burnerRole @@ -6086,7 +18722,7 @@ _AC role, owner of which can mint hypeBTC token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn hypeBTC token_ +_AC role, owner of which can burn mF-ONE token_ ### _pauserRole @@ -6094,11 +18730,11 @@ _AC role, owner of which can burn hypeBTC token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause hypeBTC token_ +_AC role, owner of which can pause mF-ONE token_ -## HypeEthCustomAggregatorFeed +## MHyperCustomAggregatorFeed -AggregatorV3 compatible feed for hypeETH, +AggregatorV3 compatible feed for mHYPER, where price is submitted manually by feed admins ### feedAdminRole @@ -6115,9 +18751,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## HypeEthDataFeed +## MHyperDataFeed -DataFeed for hypeETH product +DataFeed for mHYPER product ### feedAdminRole @@ -6133,45 +18769,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## hypeETH +## mHYPER -### HYPE_ETH_MINT_OPERATOR_ROLE +### M_HYPER_MINT_OPERATOR_ROLE ```solidity -bytes32 HYPE_ETH_MINT_OPERATOR_ROLE +bytes32 M_HYPER_MINT_OPERATOR_ROLE ``` -actor that can mint hypeETH +actor that can mint mHYPER -### HYPE_ETH_BURN_OPERATOR_ROLE +### M_HYPER_BURN_OPERATOR_ROLE ```solidity -bytes32 HYPE_ETH_BURN_OPERATOR_ROLE +bytes32 M_HYPER_BURN_OPERATOR_ROLE ``` -actor that can burn hypeETH +actor that can burn mHYPER -### HYPE_ETH_PAUSE_OPERATOR_ROLE +### M_HYPER_PAUSE_OPERATOR_ROLE ```solidity -bytes32 HYPE_ETH_PAUSE_OPERATOR_ROLE +bytes32 M_HYPER_PAUSE_OPERATOR_ROLE ``` -actor that can pause hypeETH +actor that can pause mHYPER -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControl contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -6179,7 +18816,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint hypeETH token_ +_AC role, owner of which can mint mHYPER token_ ### _burnerRole @@ -6187,7 +18824,7 @@ _AC role, owner of which can mint hypeETH token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn hypeETH token_ +_AC role, owner of which can burn mHYPER token_ ### _pauserRole @@ -6195,11 +18832,11 @@ _AC role, owner of which can burn hypeETH token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause hypeETH token_ +_AC role, owner of which can pause mHYPER token_ -## HypeUsdCustomAggregatorFeed +## MHyperBtcCustomAggregatorFeed -AggregatorV3 compatible feed for hypeUSD, +AggregatorV3 compatible feed for mHyperBTC, where price is submitted manually by feed admins ### feedAdminRole @@ -6216,9 +18853,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## HypeUsdDataFeed +## MHyperBtcDataFeed -DataFeed for hypeUSD product +DataFeed for mHyperBTC product ### feedAdminRole @@ -6234,45 +18871,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## hypeUSD +## mHyperBTC -### HYPE_USD_MINT_OPERATOR_ROLE +### M_HYPER_BTC_MINT_OPERATOR_ROLE ```solidity -bytes32 HYPE_USD_MINT_OPERATOR_ROLE +bytes32 M_HYPER_BTC_MINT_OPERATOR_ROLE ``` -actor that can mint hypeUSD +actor that can mint mHyperBTC -### HYPE_USD_BURN_OPERATOR_ROLE +### M_HYPER_BTC_BURN_OPERATOR_ROLE ```solidity -bytes32 HYPE_USD_BURN_OPERATOR_ROLE +bytes32 M_HYPER_BTC_BURN_OPERATOR_ROLE ``` -actor that can burn hypeUSD +actor that can burn mHyperBTC -### HYPE_USD_PAUSE_OPERATOR_ROLE +### M_HYPER_BTC_PAUSE_OPERATOR_ROLE ```solidity -bytes32 HYPE_USD_PAUSE_OPERATOR_ROLE +bytes32 M_HYPER_BTC_PAUSE_OPERATOR_ROLE ``` -actor that can pause hypeUSD +actor that can pause mHyperBTC -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControl contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -6280,302 +18918,265 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint hypeUSD token_ +_AC role, owner of which can mint mHyperBTC token_ ### _burnerRole ```solidity -function _burnerRole() internal pure returns (bytes32) -``` - -_AC role, owner of which can burn hypeUSD token_ - -### _pauserRole - -```solidity -function _pauserRole() internal pure returns (bytes32) -``` - -_AC role, owner of which can pause hypeUSD token_ - -## IAllowListV2 - -### EntityId - -### FundPermissionSet - -```solidity -event FundPermissionSet(IAllowListV2.EntityId entityId, string fundSymbol, bool permission) -``` - -An event emitted when an address's permission is changed for a fund. - -### ProtocolAddressPermissionSet - -```solidity -event ProtocolAddressPermissionSet(address addr, string fundSymbol, bool isAllowed) -``` - -An event emitted when a protocol's permission is changed for a fund. - -### EntityIdSet - -```solidity -event EntityIdSet(address addr, uint256 entityId) -``` - -An event emitted when an address is associated with an entityId - -### BadData - -```solidity -error BadData() +function _burnerRole() internal pure returns (bytes32) ``` -_Thrown when the input for a function is invalid_ +_AC role, owner of which can burn mHyperBTC token_ -### AlreadySet +### _pauserRole ```solidity -error AlreadySet() +function _pauserRole() internal pure returns (bytes32) ``` -_Thrown when the input is already equivalent to the storage being set_ +_AC role, owner of which can pause mHyperBTC token_ -### NonZeroEntityIdMustBeChangedToZero +## MHyperEthCustomAggregatorFeed + +AggregatorV3 compatible feed for mHyperETH, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -error NonZeroEntityIdMustBeChangedToZero() +function feedAdminRole() public pure returns (bytes32) ``` -_An address's entityId can not be changed once set, it can only be unset and then set to a new value_ +_describes a role, owner of which can update prices in this feed_ -### AddressHasProtocolPermissions +#### Return Values -```solidity -error AddressHasProtocolPermissions() -``` +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -_Thrown when trying to set entityId for an address that has protocol permissions_ +## MHyperEthDataFeed -### AddressHasEntityId +DataFeed for mHyperETH product + +### feedAdminRole ```solidity -error AddressHasEntityId() +function feedAdminRole() public pure returns (bytes32) ``` -_Thrown when trying to set protocol permissions for an address that has an entityId_ +_describes a role, owner of which can manage this feed_ -### CodeSizeZero +#### Return Values -```solidity -error CodeSizeZero() -``` +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -_Thrown when trying to set protocol permissions but the code size is 0_ +## mHyperETH -### Deprecated +### M_HYPER_ETH_MINT_OPERATOR_ROLE ```solidity -error Deprecated() +bytes32 M_HYPER_ETH_MINT_OPERATOR_ROLE ``` -_Thrown when a method is no longer supported_ +actor that can mint mHyperETH -### RenounceOwnershipDisabled +### M_HYPER_ETH_BURN_OPERATOR_ROLE ```solidity -error RenounceOwnershipDisabled() +bytes32 M_HYPER_ETH_BURN_OPERATOR_ROLE ``` -_Thrown if an attempt to call `renounceOwnership` is made_ +actor that can burn mHyperETH -### owner +### M_HYPER_ETH_PAUSE_OPERATOR_ROLE ```solidity -function owner() external view returns (address) +bytes32 M_HYPER_ETH_PAUSE_OPERATOR_ROLE ``` -### addressEntityIds +actor that can pause mHyperETH + +### _getNameSymbol ```solidity -function addressEntityIds(address addr) external view returns (IAllowListV2.EntityId) +function _getNameSymbol() internal pure returns (string, string) ``` -Gets the entityId for the provided address +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| addr | address | The address to get the entityId for | +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### isAddressAllowedForFund +### _minterRole ```solidity -function isAddressAllowedForFund(address addr, string fundSymbol) external view returns (bool) +function _minterRole() internal pure returns (bytes32) ``` -Checks whether an address is allowed to use a fund - -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| addr | address | The address to check permissions for | -| fundSymbol | string | The fund symbol to check permissions for | +_AC role, owner of which can mint mHyperETH token_ -### isEntityAllowedForFund +### _burnerRole ```solidity -function isEntityAllowedForFund(IAllowListV2.EntityId entityId, string fundSymbol) external view returns (bool) +function _burnerRole() internal pure returns (bytes32) ``` -Checks whether an Entity is allowed to use a fund - -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| entityId | IAllowListV2.EntityId | | -| fundSymbol | string | The fund symbol to check permissions for | +_AC role, owner of which can burn mHyperETH token_ -### setEntityAllowedForFund +### _pauserRole ```solidity -function setEntityAllowedForFund(IAllowListV2.EntityId entityId, string fundSymbol, bool isAllowed) external +function _pauserRole() internal pure returns (bytes32) ``` -Sets whether an Entity is allowed to use a fund +_AC role, owner of which can pause mHyperETH token_ -#### Parameters +## MKRalphaCustomAggregatorFeed -| Name | Type | Description | -| ---- | ---- | ----------- | -| entityId | IAllowListV2.EntityId | | -| fundSymbol | string | The fund symbol to set permissions for | -| isAllowed | bool | The permission value to set | +AggregatorV3 compatible feed for mKRalpha, +where price is submitted manually by feed admins -### setEntityIdForAddress +### initialize ```solidity -function setEntityIdForAddress(IAllowListV2.EntityId entityId, address addr) external +function initialize(address _accessControl, int192 _minAnswer, int192 _maxAnswer, uint256 _maxAnswerDeviation, string _description) public ``` -Sets the entityId for a given address. Setting to 0 removes the address from the allowList +upgradeable pattern contract`s initializer #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| entityId | IAllowListV2.EntityId | The entityId to associate with an address | -| addr | address | The address to associate with an entityId | +| _accessControl | address | address of MidasAccessControll contract | +| _minAnswer | int192 | init value for `minAnswer`. Should be < `_maxAnswer` | +| _maxAnswer | int192 | init value for `maxAnswer`. Should be > `_minAnswer` | +| _maxAnswerDeviation | uint256 | init value for `maxAnswerDeviation` | +| _description | string | init value for `description` | -### setEntityIdForMultipleAddresses +### initializeV2 ```solidity -function setEntityIdForMultipleAddresses(IAllowListV2.EntityId entityId, address[] addresses) external +function initializeV2(uint256 _newMaxAnswerDeviation) public ``` -Sets the entity Id for a list of addresses. Setting to 0 removes the address from the allowList +initializes the contract with a new max answer deviation + +_increases contract version to 2_ #### Parameters | Name | Type | Description | | ---- | ---- | ----------- | -| entityId | IAllowListV2.EntityId | The entityId to associate with an address | -| addresses | address[] | The addresses to associate with an entityId | +| _newMaxAnswerDeviation | uint256 | new max answer deviation | -### setProtocolAddressPermission +### feedAdminRole ```solidity -function setProtocolAddressPermission(address addr, string fundSymbol, bool isAllowed) external +function feedAdminRole() public pure returns (bytes32) ``` -Sets protocol permissions for an address +_describes a role, owner of which can update prices in this feed_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| addr | address | The address to set permissions for | -| fundSymbol | string | The fund symbol to set permissions for | -| isAllowed | bool | The permission value to set | +| [0] | bytes32 | role descriptor | -### setProtocolAddressPermissions +## MKRalphaDataFeed + +DataFeed for mKRalpha product + +### feedAdminRole ```solidity -function setProtocolAddressPermissions(address[] addresses, string fundSymbol, bool isAllowed) external +function feedAdminRole() public pure returns (bytes32) ``` -Sets protocol permissions for multiple addresses +_describes a role, owner of which can manage this feed_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| addresses | address[] | The addresses to set permissions for | -| fundSymbol | string | The fund symbol to set permissions for | -| isAllowed | bool | The permission value to set | +| [0] | bytes32 | role descriptor | -### setEntityPermissionsAndAddresses +## mKRalpha + +### M_KRALPHA_MINT_OPERATOR_ROLE ```solidity -function setEntityPermissionsAndAddresses(IAllowListV2.EntityId entityId, address[] addresses, string[] fundPermissionsToUpdate, bool[] fundPermissions) external +bytes32 M_KRALPHA_MINT_OPERATOR_ROLE ``` -Sets entity for an array of addresses and sets permissions for an entity - -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| entityId | IAllowListV2.EntityId | The entityId to be updated | -| addresses | address[] | The addresses to associate with an entityId | -| fundPermissionsToUpdate | string[] | The funds to update permissions for | -| fundPermissions | bool[] | The permissions for each fund | +actor that can mint mKRalpha -### hasAnyProtocolPermissions +### M_KRALPHA_BURN_OPERATOR_ROLE ```solidity -function hasAnyProtocolPermissions(address addr) external view returns (bool hasPermissions) +bytes32 M_KRALPHA_BURN_OPERATOR_ROLE ``` -### protocolPermissionsForFunds +actor that can burn mKRalpha + +### M_KRALPHA_PAUSE_OPERATOR_ROLE ```solidity -function protocolPermissionsForFunds(address protocol) external view returns (uint256) +bytes32 M_KRALPHA_PAUSE_OPERATOR_ROLE ``` -### protocolPermissions +actor that can pause mKRalpha + +### _getNameSymbol ```solidity -function protocolPermissions(address, string) external view returns (bool) +function _getNameSymbol() internal pure returns (string, string) ``` -### initialize +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -function initialize() external +function _minterRole() internal pure returns (bytes32) ``` -## ISuperstateToken +_AC role, owner of which can mint mKRalpha token_ -### symbol +### _burnerRole ```solidity -function symbol() external view returns (string) +function _burnerRole() internal pure returns (bytes32) ``` -### allowListV2 +_AC role, owner of which can burn mKRalpha token_ + +### _pauserRole ```solidity -function allowListV2() external view returns (address) +function _pauserRole() internal pure returns (bytes32) ``` -## MBtcCustomAggregatorFeed +_AC role, owner of which can pause mKRalpha token_ -AggregatorV3 compatible feed for mBTC, +## MLiquidityCustomAggregatorFeed + +AggregatorV3 compatible feed for mLIQUIDITY, where price is submitted manually by feed admins ### feedAdminRole @@ -6592,9 +19193,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## MBtcDataFeed +## MLiquidityDataFeed -DataFeed for mBTC product +DataFeed for mLIQUIDITY product ### feedAdminRole @@ -6610,45 +19211,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## mBTC +## mLIQUIDITY -### M_BTC_MINT_OPERATOR_ROLE +### M_LIQUIDITY_MINT_OPERATOR_ROLE ```solidity -bytes32 M_BTC_MINT_OPERATOR_ROLE +bytes32 M_LIQUIDITY_MINT_OPERATOR_ROLE ``` -actor that can mint mBTC +actor that can mint mLIQUIDITY -### M_BTC_BURN_OPERATOR_ROLE +### M_LIQUIDITY_BURN_OPERATOR_ROLE ```solidity -bytes32 M_BTC_BURN_OPERATOR_ROLE +bytes32 M_LIQUIDITY_BURN_OPERATOR_ROLE ``` -actor that can burn mBTC +actor that can burn mLIQUIDITY -### M_BTC_PAUSE_OPERATOR_ROLE +### M_LIQUIDITY_PAUSE_OPERATOR_ROLE ```solidity -bytes32 M_BTC_PAUSE_OPERATOR_ROLE +bytes32 M_LIQUIDITY_PAUSE_OPERATOR_ROLE ``` -actor that can pause mBTC +actor that can pause mLIQUIDITY -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -6656,7 +19258,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint mBTC token_ +_AC role, owner of which can mint mLIQUIDITY token_ ### _burnerRole @@ -6664,7 +19266,7 @@ _AC role, owner of which can mint mBTC token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn mBTC token_ +_AC role, owner of which can burn mLIQUIDITY token_ ### _pauserRole @@ -6672,47 +19274,85 @@ _AC role, owner of which can burn mBTC token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause mBTC token_ +_AC role, owner of which can pause mLIQUIDITY token_ -## TACmBTC +## MM1UsdCustomAggregatorFeed -### TAC_M_BTC_MINT_OPERATOR_ROLE +AggregatorV3 compatible feed for mM1USD, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -bytes32 TAC_M_BTC_MINT_OPERATOR_ROLE +function feedAdminRole() public pure returns (bytes32) ``` -actor that can mint TACmBTC +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## MM1UsdDataFeed + +DataFeed for mM1USD product + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## mM1USD + +### M_M1_USD_MINT_OPERATOR_ROLE + +```solidity +bytes32 M_M1_USD_MINT_OPERATOR_ROLE +``` + +actor that can mint mM1USD -### TAC_M_BTC_BURN_OPERATOR_ROLE +### M_M1_USD_BURN_OPERATOR_ROLE ```solidity -bytes32 TAC_M_BTC_BURN_OPERATOR_ROLE +bytes32 M_M1_USD_BURN_OPERATOR_ROLE ``` -actor that can burn TACmBTC +actor that can burn mM1USD -### TAC_M_BTC_PAUSE_OPERATOR_ROLE +### M_M1_USD_PAUSE_OPERATOR_ROLE ```solidity -bytes32 TAC_M_BTC_PAUSE_OPERATOR_ROLE +bytes32 M_M1_USD_PAUSE_OPERATOR_ROLE ``` -actor that can pause TACmBTC +actor that can pause mM1USD -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -6720,7 +19360,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint TACmBTC token_ +_AC role, owner of which can mint mM1USD token_ ### _burnerRole @@ -6728,7 +19368,7 @@ _AC role, owner of which can mint TACmBTC token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn TACmBTC token_ +_AC role, owner of which can burn mM1USD token_ ### _pauserRole @@ -6736,11 +19376,11 @@ _AC role, owner of which can burn TACmBTC token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause TACmBTC token_ +_AC role, owner of which can pause mM1USD token_ -## MBasisCustomAggregatorFeed +## MMevCustomAggregatorFeed -AggregatorV3 compatible feed for mBASIS, +AggregatorV3 compatible feed for mMEV, where price is submitted manually by feed admins ### feedAdminRole @@ -6757,9 +19397,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## MBasisDataFeed +## MMevDataFeed -DataFeed for mBASIS product +DataFeed for mMEV product ### feedAdminRole @@ -6775,45 +19415,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## mBASIS +## mMEV -### M_BASIS_MINT_OPERATOR_ROLE +### M_MEV_MINT_OPERATOR_ROLE ```solidity -bytes32 M_BASIS_MINT_OPERATOR_ROLE +bytes32 M_MEV_MINT_OPERATOR_ROLE ``` -actor that can mint mBASIS +actor that can mint mMEV -### M_BASIS_BURN_OPERATOR_ROLE +### M_MEV_BURN_OPERATOR_ROLE ```solidity -bytes32 M_BASIS_BURN_OPERATOR_ROLE +bytes32 M_MEV_BURN_OPERATOR_ROLE ``` -actor that can burn mBASIS +actor that can burn mMEV -### M_BASIS_PAUSE_OPERATOR_ROLE +### M_MEV_PAUSE_OPERATOR_ROLE ```solidity -bytes32 M_BASIS_PAUSE_OPERATOR_ROLE +bytes32 M_MEV_PAUSE_OPERATOR_ROLE ``` -actor that can pause mBASIS +actor that can pause mMEV -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -6821,7 +19462,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint mBASIS token_ +_AC role, owner of which can mint mMEV token_ ### _burnerRole @@ -6829,7 +19470,7 @@ _AC role, owner of which can mint mBASIS token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn mBASIS token_ +_AC role, owner of which can burn mMEV token_ ### _pauserRole @@ -6837,148 +19478,150 @@ _AC role, owner of which can burn mBASIS token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause mBASIS token_ - -## MEdgeCustomAggregatorFeed +_AC role, owner of which can pause mMEV token_ -AggregatorV3 compatible feed for mEDGE, -where price is submitted manually by feed admins +## TACmMEV -### feedAdminRole +### TAC_M_MEV_MINT_OPERATOR_ROLE ```solidity -function feedAdminRole() public pure returns (bytes32) +bytes32 TAC_M_MEV_MINT_OPERATOR_ROLE ``` -_describes a role, owner of which can update prices in this feed_ +actor that can mint TACmMEV -#### Return Values +### TAC_M_MEV_BURN_OPERATOR_ROLE -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | bytes32 | role descriptor | +```solidity +bytes32 TAC_M_MEV_BURN_OPERATOR_ROLE +``` -## MEdgeDataFeed +actor that can burn TACmMEV -DataFeed for mEDGE product +### TAC_M_MEV_PAUSE_OPERATOR_ROLE -### feedAdminRole +```solidity +bytes32 TAC_M_MEV_PAUSE_OPERATOR_ROLE +``` + +actor that can pause TACmMEV + +### _getNameSymbol ```solidity -function feedAdminRole() public pure returns (bytes32) +function _getNameSymbol() internal pure returns (string, string) ``` -_describes a role, owner of which can manage this feed_ +_returns name and symbol of the token_ #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | bytes32 | role descriptor | +| [0] | string | name of the token | +| [1] | string | symbol of the token | -## mEDGE - -### M_EDGE_MINT_OPERATOR_ROLE +### _minterRole ```solidity -bytes32 M_EDGE_MINT_OPERATOR_ROLE +function _minterRole() internal pure returns (bytes32) ``` -actor that can mint mEDGE +_AC role, owner of which can mint TACmMEV token_ -### M_EDGE_BURN_OPERATOR_ROLE +### _burnerRole ```solidity -bytes32 M_EDGE_BURN_OPERATOR_ROLE +function _burnerRole() internal pure returns (bytes32) ``` -actor that can burn mEDGE +_AC role, owner of which can burn TACmMEV token_ -### M_EDGE_PAUSE_OPERATOR_ROLE +### _pauserRole ```solidity -bytes32 M_EDGE_PAUSE_OPERATOR_ROLE +function _pauserRole() internal pure returns (bytes32) ``` -actor that can pause mEDGE +_AC role, owner of which can pause TACmMEV token_ -### initialize +## MPortofinoCustomAggregatorFeed + +AggregatorV3 compatible feed for mPortofino, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -function initialize(address _accessControl) external +function feedAdminRole() public pure returns (bytes32) ``` -upgradeable pattern contract`s initializer +_describes a role, owner of which can update prices in this feed_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | - -### _minterRole +| [0] | bytes32 | role descriptor | -```solidity -function _minterRole() internal pure returns (bytes32) -``` +## MPortofinoDataFeed -_AC role, owner of which can mint mEDGE token_ +DataFeed for mPortofino product -### _burnerRole +### feedAdminRole ```solidity -function _burnerRole() internal pure returns (bytes32) +function feedAdminRole() public pure returns (bytes32) ``` -_AC role, owner of which can burn mEDGE token_ - -### _pauserRole +_describes a role, owner of which can manage this feed_ -```solidity -function _pauserRole() internal pure returns (bytes32) -``` +#### Return Values -_AC role, owner of which can pause mEDGE token_ +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -## TACmEDGE +## mPortofino -### TAC_M_EDGE_MINT_OPERATOR_ROLE +### M_PORTOFINO_MINT_OPERATOR_ROLE ```solidity -bytes32 TAC_M_EDGE_MINT_OPERATOR_ROLE +bytes32 M_PORTOFINO_MINT_OPERATOR_ROLE ``` -actor that can mint TACmEDGE +actor that can mint mPortofino -### TAC_M_EDGE_BURN_OPERATOR_ROLE +### M_PORTOFINO_BURN_OPERATOR_ROLE ```solidity -bytes32 TAC_M_EDGE_BURN_OPERATOR_ROLE +bytes32 M_PORTOFINO_BURN_OPERATOR_ROLE ``` -actor that can burn TACmEDGE +actor that can burn mPortofino -### TAC_M_EDGE_PAUSE_OPERATOR_ROLE +### M_PORTOFINO_PAUSE_OPERATOR_ROLE ```solidity -bytes32 TAC_M_EDGE_PAUSE_OPERATOR_ROLE +bytes32 M_PORTOFINO_PAUSE_OPERATOR_ROLE ``` -actor that can pause TACmEDGE +actor that can pause mPortofino -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -6986,7 +19629,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint TACmEDGE token_ +_AC role, owner of which can mint mPortofino token_ ### _burnerRole @@ -6994,7 +19637,7 @@ _AC role, owner of which can mint TACmEDGE token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn TACmEDGE token_ +_AC role, owner of which can burn mPortofino token_ ### _pauserRole @@ -7002,13 +19645,47 @@ _AC role, owner of which can burn TACmEDGE token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause TACmEDGE token_ +_AC role, owner of which can pause mPortofino token_ -## MFOneCustomAggregatorFeed +## MRe7CustomAggregatorFeed -AggregatorV3 compatible feed for mF-ONE, +AggregatorV3 compatible feed for mRE7, where price is submitted manually by feed admins +### initialize + +```solidity +function initialize(address _accessControl, int192 _minAnswer, int192 _maxAnswer, uint256 _maxAnswerDeviation, string _description) public +``` + +upgradeable pattern contract`s initializer + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _accessControl | address | address of MidasAccessControll contract | +| _minAnswer | int192 | init value for `minAnswer`. Should be < `_maxAnswer` | +| _maxAnswer | int192 | init value for `maxAnswer`. Should be > `_minAnswer` | +| _maxAnswerDeviation | uint256 | init value for `maxAnswerDeviation` | +| _description | string | init value for `description` | + +### initializeV3 + +```solidity +function initializeV3(uint256 _newMaxAnswerDeviation) public +``` + +initializes the contract with a new max answer deviation + +_increases contract version to 3 (2 was already used)_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| _newMaxAnswerDeviation | uint256 | new max answer deviation | + ### feedAdminRole ```solidity @@ -7023,9 +19700,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## MFOneDataFeed +## MRe7DataFeed -DataFeed for mF-ONE product +DataFeed for mRE7 product ### feedAdminRole @@ -7041,45 +19718,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## mFONE +## mRE7 -### M_FONE_MINT_OPERATOR_ROLE +### M_RE7_MINT_OPERATOR_ROLE ```solidity -bytes32 M_FONE_MINT_OPERATOR_ROLE +bytes32 M_RE7_MINT_OPERATOR_ROLE ``` -actor that can mint mF-ONE +actor that can mint mRE7 -### M_FONE_BURN_OPERATOR_ROLE +### M_RE7_BURN_OPERATOR_ROLE ```solidity -bytes32 M_FONE_BURN_OPERATOR_ROLE +bytes32 M_RE7_BURN_OPERATOR_ROLE ``` -actor that can burn mF-ONE +actor that can burn mRE7 -### M_FONE_PAUSE_OPERATOR_ROLE +### M_RE7_PAUSE_OPERATOR_ROLE ```solidity -bytes32 M_FONE_PAUSE_OPERATOR_ROLE +bytes32 M_RE7_PAUSE_OPERATOR_ROLE ``` -actor that can pause mF-ONE +actor that can pause mRE7 -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControl contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -7087,7 +19765,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint mF-ONE token_ +_AC role, owner of which can mint mRE7 token_ ### _burnerRole @@ -7095,7 +19773,7 @@ _AC role, owner of which can mint mF-ONE token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn mF-ONE token_ +_AC role, owner of which can burn mRE7 token_ ### _pauserRole @@ -7103,11 +19781,11 @@ _AC role, owner of which can burn mF-ONE token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause mF-ONE token_ +_AC role, owner of which can pause mRE7 token_ -## MLiquidityCustomAggregatorFeed +## MRe7BtcCustomAggregatorFeed -AggregatorV3 compatible feed for mLIQUIDITY, +AggregatorV3 compatible feed for mRE7BTC, where price is submitted manually by feed admins ### feedAdminRole @@ -7124,9 +19802,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## MLiquidityDataFeed +## MRe7BtcDataFeed -DataFeed for mLIQUIDITY product +DataFeed for mRE7BTC product ### feedAdminRole @@ -7142,45 +19820,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## mLIQUIDITY +## mRE7BTC -### M_LIQUIDITY_MINT_OPERATOR_ROLE +### M_RE7BTC_MINT_OPERATOR_ROLE ```solidity -bytes32 M_LIQUIDITY_MINT_OPERATOR_ROLE +bytes32 M_RE7BTC_MINT_OPERATOR_ROLE ``` -actor that can mint mLIQUIDITY +actor that can mint mRE7BTC -### M_LIQUIDITY_BURN_OPERATOR_ROLE +### M_RE7BTC_BURN_OPERATOR_ROLE ```solidity -bytes32 M_LIQUIDITY_BURN_OPERATOR_ROLE +bytes32 M_RE7BTC_BURN_OPERATOR_ROLE ``` -actor that can burn mLIQUIDITY +actor that can burn mRE7BTC -### M_LIQUIDITY_PAUSE_OPERATOR_ROLE +### M_RE7BTC_PAUSE_OPERATOR_ROLE ```solidity -bytes32 M_LIQUIDITY_PAUSE_OPERATOR_ROLE +bytes32 M_RE7BTC_PAUSE_OPERATOR_ROLE ``` -actor that can pause mLIQUIDITY +actor that can pause mRE7BTC -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -7188,7 +19867,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint mLIQUIDITY token_ +_AC role, owner of which can mint mRE7BTC token_ ### _burnerRole @@ -7196,7 +19875,7 @@ _AC role, owner of which can mint mLIQUIDITY token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn mLIQUIDITY token_ +_AC role, owner of which can burn mRE7BTC token_ ### _pauserRole @@ -7204,11 +19883,11 @@ _AC role, owner of which can burn mLIQUIDITY token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause mLIQUIDITY token_ +_AC role, owner of which can pause mRE7BTC token_ -## MMevCustomAggregatorFeed +## MRe7SolCustomAggregatorFeed -AggregatorV3 compatible feed for mMEV, +AggregatorV3 compatible feed for mRE7SOL, where price is submitted manually by feed admins ### feedAdminRole @@ -7225,9 +19904,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## MMevDataFeed +## MRe7SolDataFeed -DataFeed for mMEV product +DataFeed for mRE7SOL product ### feedAdminRole @@ -7243,45 +19922,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## mMEV +## mRE7SOL -### M_MEV_MINT_OPERATOR_ROLE +### M_RE7SOL_MINT_OPERATOR_ROLE ```solidity -bytes32 M_MEV_MINT_OPERATOR_ROLE +bytes32 M_RE7SOL_MINT_OPERATOR_ROLE ``` -actor that can mint mMEV +actor that can mint mRE7SOL -### M_MEV_BURN_OPERATOR_ROLE +### M_RE7SOL_BURN_OPERATOR_ROLE ```solidity -bytes32 M_MEV_BURN_OPERATOR_ROLE +bytes32 M_RE7SOL_BURN_OPERATOR_ROLE ``` -actor that can burn mMEV +actor that can burn mRE7SOL -### M_MEV_PAUSE_OPERATOR_ROLE +### M_RE7SOL_PAUSE_OPERATOR_ROLE ```solidity -bytes32 M_MEV_PAUSE_OPERATOR_ROLE +bytes32 M_RE7SOL_PAUSE_OPERATOR_ROLE ``` -actor that can pause mMEV +actor that can pause mRE7SOL -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -7289,7 +19969,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint mMEV token_ +_AC role, owner of which can mint mRE7SOL token_ ### _burnerRole @@ -7297,7 +19977,7 @@ _AC role, owner of which can mint mMEV token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn mMEV token_ +_AC role, owner of which can burn mRE7SOL token_ ### _pauserRole @@ -7305,47 +19985,85 @@ _AC role, owner of which can burn mMEV token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause mMEV token_ +_AC role, owner of which can pause mRE7SOL token_ -## TACmMEV +## MRoxCustomAggregatorFeed -### TAC_M_MEV_MINT_OPERATOR_ROLE +AggregatorV3 compatible feed for mROX, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -bytes32 TAC_M_MEV_MINT_OPERATOR_ROLE +function feedAdminRole() public pure returns (bytes32) ``` -actor that can mint TACmMEV +_describes a role, owner of which can update prices in this feed_ -### TAC_M_MEV_BURN_OPERATOR_ROLE +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## MRoxDataFeed + +DataFeed for mROX product + +### feedAdminRole ```solidity -bytes32 TAC_M_MEV_BURN_OPERATOR_ROLE +function feedAdminRole() public pure returns (bytes32) ``` -actor that can burn TACmMEV +_describes a role, owner of which can manage this feed_ -### TAC_M_MEV_PAUSE_OPERATOR_ROLE +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## mROX + +### M_ROX_MINT_OPERATOR_ROLE ```solidity -bytes32 TAC_M_MEV_PAUSE_OPERATOR_ROLE +bytes32 M_ROX_MINT_OPERATOR_ROLE ``` -actor that can pause TACmMEV +actor that can mint mROX -### initialize +### M_ROX_BURN_OPERATOR_ROLE ```solidity -function initialize(address _accessControl) external +bytes32 M_ROX_BURN_OPERATOR_ROLE ``` -upgradeable pattern contract`s initializer +actor that can burn mROX -#### Parameters +### M_ROX_PAUSE_OPERATOR_ROLE + +```solidity +bytes32 M_ROX_PAUSE_OPERATOR_ROLE +``` + +actor that can pause mROX + +### _getNameSymbol + +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -7353,7 +20071,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint TACmMEV token_ +_AC role, owner of which can mint mROX token_ ### _burnerRole @@ -7361,7 +20079,7 @@ _AC role, owner of which can mint TACmMEV token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn TACmMEV token_ +_AC role, owner of which can burn mROX token_ ### _pauserRole @@ -7369,11 +20087,11 @@ _AC role, owner of which can burn TACmMEV token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause TACmMEV token_ +_AC role, owner of which can pause mROX token_ -## MRe7CustomAggregatorFeed +## MSlCustomAggregatorFeed -AggregatorV3 compatible feed for mRE7, +AggregatorV3 compatible feed for mSL, where price is submitted manually by feed admins ### feedAdminRole @@ -7390,9 +20108,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## MRe7DataFeed +## MSlDataFeed -DataFeed for mRE7 product +DataFeed for mSL product ### feedAdminRole @@ -7408,45 +20126,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## mRE7 +## mSL -### M_RE7_MINT_OPERATOR_ROLE +### M_SL_MINT_OPERATOR_ROLE ```solidity -bytes32 M_RE7_MINT_OPERATOR_ROLE +bytes32 M_SL_MINT_OPERATOR_ROLE ``` -actor that can mint mRE7 +actor that can mint mSL -### M_RE7_BURN_OPERATOR_ROLE +### M_SL_BURN_OPERATOR_ROLE ```solidity -bytes32 M_RE7_BURN_OPERATOR_ROLE +bytes32 M_SL_BURN_OPERATOR_ROLE ``` -actor that can burn mRE7 +actor that can burn mSL -### M_RE7_PAUSE_OPERATOR_ROLE +### M_SL_PAUSE_OPERATOR_ROLE ```solidity -bytes32 M_RE7_PAUSE_OPERATOR_ROLE +bytes32 M_SL_PAUSE_OPERATOR_ROLE ``` -actor that can pause mRE7 +actor that can pause mSL -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -7454,7 +20173,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint mRE7 token_ +_AC role, owner of which can mint mSL token_ ### _burnerRole @@ -7462,7 +20181,7 @@ _AC role, owner of which can mint mRE7 token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn mRE7 token_ +_AC role, owner of which can burn mSL token_ ### _pauserRole @@ -7470,11 +20189,11 @@ _AC role, owner of which can burn mRE7 token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause mRE7 token_ +_AC role, owner of which can pause mSL token_ -## MRe7SolCustomAggregatorFeed +## MTBillCustomAggregatorFeed -AggregatorV3 compatible feed for mRE7SOL, +AggregatorV3 compatible feed for mTBILL, where price is submitted manually by feed admins ### feedAdminRole @@ -7491,9 +20210,29 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## MRe7SolDataFeed +## MTBillCustomAggregatorFeedGrowth -DataFeed for mRE7SOL product +AggregatorV3 compatible feed for mTBILL, +where price is submitted manually by feed admins, +and growth apr applies to the answer. + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## MTBillDataFeed + +DataFeed for mTBILL product ### feedAdminRole @@ -7509,45 +20248,58 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## mRE7SOL +## MTBillMidasAccessControlRoles -### M_RE7SOL_MINT_OPERATOR_ROLE +Base contract that stores all roles descriptors for mTBILL contracts + +### M_TBILL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE ```solidity -bytes32 M_RE7SOL_MINT_OPERATOR_ROLE +bytes32 M_TBILL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE ``` -actor that can mint mRE7SOL +actor that can manage MTBillCustomAggregatorFeed and MTBillDataFeed -### M_RE7SOL_BURN_OPERATOR_ROLE +## mTBILL + +### M_TBILL_MINT_OPERATOR_ROLE ```solidity -bytes32 M_RE7SOL_BURN_OPERATOR_ROLE +bytes32 M_TBILL_MINT_OPERATOR_ROLE ``` -actor that can burn mRE7SOL +actor that can mint mTBILL -### M_RE7SOL_PAUSE_OPERATOR_ROLE +### M_TBILL_BURN_OPERATOR_ROLE ```solidity -bytes32 M_RE7SOL_PAUSE_OPERATOR_ROLE +bytes32 M_TBILL_BURN_OPERATOR_ROLE ``` -actor that can pause mRE7SOL +actor that can burn mTBILL -### initialize +### M_TBILL_PAUSE_OPERATOR_ROLE ```solidity -function initialize(address _accessControl) external +bytes32 M_TBILL_PAUSE_OPERATOR_ROLE ``` -upgradeable pattern contract`s initializer +actor that can pause mTBILL -#### Parameters +### _getNameSymbol + +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -7555,7 +20307,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint mRE7SOL token_ +_AC role, owner of which can mint mTBILL token_ ### _burnerRole @@ -7563,7 +20315,7 @@ _AC role, owner of which can mint mRE7SOL token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn mRE7SOL token_ +_AC role, owner of which can burn mTBILL token_ ### _pauserRole @@ -7571,11 +20323,11 @@ _AC role, owner of which can burn mRE7SOL token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause mRE7SOL token_ +_AC role, owner of which can pause mTBILL token_ -## MSlCustomAggregatorFeed +## MTuCustomAggregatorFeed -AggregatorV3 compatible feed for mSL, +AggregatorV3 compatible feed for mTU, where price is submitted manually by feed admins ### feedAdminRole @@ -7592,9 +20344,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## MSlDataFeed +## MTuDataFeed -DataFeed for mSL product +DataFeed for mTU product ### feedAdminRole @@ -7610,45 +20362,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## mSL +## mTU -### M_SL_MINT_OPERATOR_ROLE +### M_TU_MINT_OPERATOR_ROLE ```solidity -bytes32 M_SL_MINT_OPERATOR_ROLE +bytes32 M_TU_MINT_OPERATOR_ROLE ``` -actor that can mint mSL +actor that can mint mTU -### M_SL_BURN_OPERATOR_ROLE +### M_TU_BURN_OPERATOR_ROLE ```solidity -bytes32 M_SL_BURN_OPERATOR_ROLE +bytes32 M_TU_BURN_OPERATOR_ROLE ``` -actor that can burn mSL +actor that can burn mTU -### M_SL_PAUSE_OPERATOR_ROLE +### M_TU_PAUSE_OPERATOR_ROLE ```solidity -bytes32 M_SL_PAUSE_OPERATOR_ROLE +bytes32 M_TU_PAUSE_OPERATOR_ROLE ``` -actor that can pause mSL +actor that can pause mTU -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -7656,7 +20409,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint mSL token_ +_AC role, owner of which can mint mTU token_ ### _burnerRole @@ -7664,7 +20417,7 @@ _AC role, owner of which can mint mSL token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn mSL token_ +_AC role, owner of which can burn mTU token_ ### _pauserRole @@ -7672,11 +20425,11 @@ _AC role, owner of which can burn mSL token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause mSL token_ +_AC role, owner of which can pause mTU token_ -## MTBillCustomAggregatorFeed +## MWildUsdCustomAggregatorFeed -AggregatorV3 compatible feed for mTBILL, +AggregatorV3 compatible feed for mWildUSD, where price is submitted manually by feed admins ### feedAdminRole @@ -7693,9 +20446,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## MTBillDataFeed +## MWildUsdDataFeed -DataFeed for mBASIS product +DataFeed for mWildUSD product ### feedAdminRole @@ -7711,140 +20464,172 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## MTBillMidasAccessControlRoles +## mWildUSD -Base contract that stores all roles descriptors for mBASIS contracts +### M_WILD_USD_MINT_OPERATOR_ROLE -### M_TBILL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +```solidity +bytes32 M_WILD_USD_MINT_OPERATOR_ROLE +``` + +actor that can mint mWildUSD + +### M_WILD_USD_BURN_OPERATOR_ROLE ```solidity -bytes32 M_TBILL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +bytes32 M_WILD_USD_BURN_OPERATOR_ROLE ``` -actor that can manage MTBillCustomAggregatorFeed and MTBillDataFeed +actor that can burn mWildUSD -## mTBILL +### M_WILD_USD_PAUSE_OPERATOR_ROLE -### metadata +```solidity +bytes32 M_WILD_USD_PAUSE_OPERATOR_ROLE +``` + +actor that can pause mWildUSD + +### _getNameSymbol ```solidity -mapping(bytes32 => bytes) metadata +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole + +```solidity +function _minterRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can mint mWildUSD token_ + +### _burnerRole + +```solidity +function _burnerRole() internal pure returns (bytes32) ``` -metadata key => metadata value +_AC role, owner of which can burn mWildUSD token_ -### initialize +### _pauserRole ```solidity -function initialize(address _accessControl) external virtual +function _pauserRole() internal pure returns (bytes32) ``` -upgradeable pattern contract`s initializer +_AC role, owner of which can pause mWildUSD token_ -#### Parameters +## MXrpCustomAggregatorFeed -| Name | Type | Description | -| ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +AggregatorV3 compatible feed for mXRP, +where price is submitted manually by feed admins -### mint +### feedAdminRole ```solidity -function mint(address to, uint256 amount) external +function feedAdminRole() public pure returns (bytes32) ``` -mints mTBILL token `amount` to a given `to` address. -should be called only from permissioned actor +_describes a role, owner of which can update prices in this feed_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| to | address | addres to mint tokens to | -| amount | uint256 | amount to mint | +| [0] | bytes32 | role descriptor | -### burn +## MXrpDataFeed + +DataFeed for mXRP product + +### feedAdminRole ```solidity -function burn(address from, uint256 amount) external +function feedAdminRole() public pure returns (bytes32) ``` -burns mTBILL token `amount` to a given `to` address. -should be called only from permissioned actor +_describes a role, owner of which can manage this feed_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| from | address | addres to burn tokens from | -| amount | uint256 | amount to burn | +| [0] | bytes32 | role descriptor | -### pause +## mXRP + +### M_XRP_MINT_OPERATOR_ROLE ```solidity -function pause() external +bytes32 M_XRP_MINT_OPERATOR_ROLE ``` -puts mTBILL token on pause. -should be called only from permissioned actor +actor that can mint mXRP -### unpause +### M_XRP_BURN_OPERATOR_ROLE ```solidity -function unpause() external +bytes32 M_XRP_BURN_OPERATOR_ROLE ``` -puts mTBILL token on pause. -should be called only from permissioned actor +actor that can burn mXRP -### setMetadata +### M_XRP_PAUSE_OPERATOR_ROLE ```solidity -function setMetadata(bytes32 key, bytes data) external +bytes32 M_XRP_PAUSE_OPERATOR_ROLE ``` -updates contract`s metadata. -should be called only from permissioned actor - -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| key | bytes32 | metadata map. key | -| data | bytes | metadata map. value | +actor that can pause mXRP -### _beforeTokenTransfer +### _getNameSymbol ```solidity -function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual +function _getNameSymbol() internal pure returns (string, string) ``` -_overrides _beforeTokenTransfer function to ban -blaclisted users from using the token functions_ +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole ```solidity -function _minterRole() internal pure virtual returns (bytes32) +function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint mTBILL token_ +_AC role, owner of which can mint mXRP token_ ### _burnerRole ```solidity -function _burnerRole() internal pure virtual returns (bytes32) +function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn mTBILL token_ +_AC role, owner of which can burn mXRP token_ ### _pauserRole ```solidity -function _pauserRole() internal pure virtual returns (bytes32) +function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause mTBILL token_ +_AC role, owner of which can pause mXRP token_ ## MevBtcCustomAggregatorFeed @@ -7909,19 +20694,20 @@ bytes32 MEV_BTC_PAUSE_OPERATOR_ROLE actor that can pause mevBTC -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControll contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -7947,1062 +20733,1335 @@ function _pauserRole() internal pure returns (bytes32) _AC role, owner of which can pause mevBTC token_ -## IStdReference +## MSyrupUsdCustomAggregatorFeed -### ReferenceData +AggregatorV3 compatible feed for msyrupUSD, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -struct ReferenceData { - uint256 rate; - uint256 lastUpdatedBase; - uint256 lastUpdatedQuote; -} +function feedAdminRole() public pure returns (bytes32) ``` -### getReferenceData +_describes a role, owner of which can update prices in this feed_ -```solidity -function getReferenceData(string _base, string _quote) external view returns (struct IStdReference.ReferenceData) -``` +#### Return Values -Returns the price data for the given base/quote pair. Revert if not available. +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -### getReferenceDataBulk +## MSyrupUsdDataFeed + +DataFeed for msyrupUSD product + +### feedAdminRole ```solidity -function getReferenceDataBulk(string[] _bases, string[] _quotes) external view returns (struct IStdReference.ReferenceData[]) +function feedAdminRole() public pure returns (bytes32) ``` -Similar to getReferenceData, but with multiple base/quote pairs at once. +_describes a role, owner of which can manage this feed_ -## BandStdChailinkAdapter +#### Return Values -### ref +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -contract IStdReference ref -``` +## msyrupUSD -### base +### M_SYRUP_USD_MINT_OPERATOR_ROLE ```solidity -string base +bytes32 M_SYRUP_USD_MINT_OPERATOR_ROLE ``` -### quote +actor that can mint msyrupUSD + +### M_SYRUP_USD_BURN_OPERATOR_ROLE ```solidity -string quote +bytes32 M_SYRUP_USD_BURN_OPERATOR_ROLE ``` -### constructor +actor that can burn msyrupUSD + +### M_SYRUP_USD_PAUSE_OPERATOR_ROLE ```solidity -constructor(address _ref, string _base, string _quote) public +bytes32 M_SYRUP_USD_PAUSE_OPERATOR_ROLE ``` -### decimals +actor that can pause msyrupUSD + +### _getNameSymbol ```solidity -function decimals() external pure returns (uint8) +function _getNameSymbol() internal pure returns (string, string) ``` -### description +_returns name and symbol of the token_ -```solidity -function description() public pure returns (string) -``` +#### Return Values -### version +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -function version() public pure returns (uint256) +function _minterRole() internal pure returns (bytes32) ``` -### latestAnswer +_AC role, owner of which can mint msyrupUSD token_ + +### _burnerRole ```solidity -function latestAnswer() public view virtual returns (int256) +function _burnerRole() internal pure returns (bytes32) ``` -### latestTimestamp +_AC role, owner of which can burn msyrupUSD token_ + +### _pauserRole ```solidity -function latestTimestamp() public view returns (uint256) +function _pauserRole() internal pure returns (bytes32) ``` -### latestRound +_AC role, owner of which can pause msyrupUSD token_ + +## MSyrupUsdpCustomAggregatorFeed + +AggregatorV3 compatible feed for msyrupUSDp, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -function latestRound() public view returns (uint256) +function feedAdminRole() public pure returns (bytes32) ``` -### getAnswer +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## MSyrupUsdpDataFeed + +DataFeed for msyrupUSDp product + +### feedAdminRole ```solidity -function getAnswer(uint256) public view returns (int256) +function feedAdminRole() public pure returns (bytes32) ``` -### getTimestamp +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## msyrupUSDp + +### M_SYRUP_USDP_MINT_OPERATOR_ROLE ```solidity -function getTimestamp(uint256) external view returns (uint256) +bytes32 M_SYRUP_USDP_MINT_OPERATOR_ROLE ``` -### getRoundData +actor that can mint msyrupUSDp + +### M_SYRUP_USDP_BURN_OPERATOR_ROLE ```solidity -function getRoundData(uint80 _roundId) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +bytes32 M_SYRUP_USDP_BURN_OPERATOR_ROLE ``` -### latestRoundData +actor that can burn msyrupUSDp + +### M_SYRUP_USDP_PAUSE_OPERATOR_ROLE ```solidity -function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +bytes32 M_SYRUP_USDP_PAUSE_OPERATOR_ROLE ``` -## IMantleLspStaking +actor that can pause msyrupUSDp -### mETHToETH +### _getNameSymbol ```solidity -function mETHToETH(uint256 mETHAmount) external view returns (uint256) +function _getNameSymbol() internal pure returns (string, string) ``` -## MantleLspStakingAdapter +_returns name and symbol of the token_ -example https://etherscan.io/address/0xe3cBd06D7dadB3F4e6557bAb7EdD924CD1489E8f +#### Return Values -### lspStaking +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -contract IMantleLspStaking lspStaking +function _minterRole() internal pure returns (bytes32) ``` -### constructor +_AC role, owner of which can mint msyrupUSDp token_ + +### _burnerRole ```solidity -constructor(address _lspStaking) public +function _burnerRole() internal pure returns (bytes32) ``` -### decimals +_AC role, owner of which can burn msyrupUSDp token_ + +### _pauserRole ```solidity -function decimals() external pure returns (uint8) +function _pauserRole() internal pure returns (bytes32) ``` -### description +_AC role, owner of which can pause msyrupUSDp token_ -```solidity -function description() public pure returns (string) -``` +## ObeatUsdCustomAggregatorFeed -### version +AggregatorV3 compatible feed for obeatUSD, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -function version() public pure returns (uint256) +function feedAdminRole() public pure returns (bytes32) ``` -### latestAnswer +_describes a role, owner of which can update prices in this feed_ -```solidity -function latestAnswer() public view virtual returns (int256) -``` +#### Return Values -### latestTimestamp +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## ObeatUsdDataFeed + +DataFeed for obeatUSD product + +### feedAdminRole ```solidity -function latestTimestamp() public view returns (uint256) +function feedAdminRole() public pure returns (bytes32) ``` -### latestRound +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## obeatUSD + +### OBEAT_USD_MINT_OPERATOR_ROLE ```solidity -function latestRound() public view returns (uint256) +bytes32 OBEAT_USD_MINT_OPERATOR_ROLE ``` -### getAnswer +actor that can mint obeatUSD + +### OBEAT_USD_BURN_OPERATOR_ROLE ```solidity -function getAnswer(uint256) public view returns (int256) +bytes32 OBEAT_USD_BURN_OPERATOR_ROLE ``` -### getTimestamp +actor that can burn obeatUSD + +### OBEAT_USD_PAUSE_OPERATOR_ROLE ```solidity -function getTimestamp(uint256) external view returns (uint256) +bytes32 OBEAT_USD_PAUSE_OPERATOR_ROLE ``` -### getRoundData +actor that can pause obeatUSD + +### _getNameSymbol ```solidity -function getRoundData(uint80) external view returns (uint80, int256, uint256, uint256, uint80) +function _getNameSymbol() internal pure returns (string, string) ``` -### latestRoundData +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +function _minterRole() internal pure returns (bytes32) ``` -## PythChainlinkAdapter +_AC role, owner of which can mint obeatUSD token_ -### constructor +### _burnerRole ```solidity -constructor(address _pyth, bytes32 _priceId) public +function _burnerRole() internal pure returns (bytes32) ``` -## IRsEth +_AC role, owner of which can burn obeatUSD token_ -### rsETHPrice +### _pauserRole ```solidity -function rsETHPrice() external view returns (uint256) +function _pauserRole() internal pure returns (bytes32) ``` -## RsEthAdapter +_AC role, owner of which can pause obeatUSD token_ -example https://etherscan.io/address/0x349A73444b1a310BAe67ef67973022020d70020d +## PlUsdCustomAggregatorFeed -### rsEth +AggregatorV3 compatible feed for plUSD, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -contract IRsEth rsEth +function feedAdminRole() public pure returns (bytes32) ``` -### constructor +_describes a role, owner of which can update prices in this feed_ -```solidity -constructor(address _rsEth) public -``` +#### Return Values -### decimals +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -function decimals() external pure returns (uint8) -``` +## PlUsdDataFeed -### description +DataFeed for plUSD product + +### feedAdminRole ```solidity -function description() public pure returns (string) +function feedAdminRole() public pure returns (bytes32) ``` -### version +_describes a role, owner of which can manage this feed_ -```solidity -function version() public pure returns (uint256) -``` +#### Return Values -### latestAnswer +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -function latestAnswer() public view virtual returns (int256) -``` +## plUSD -### latestTimestamp +### PL_USD_MINT_OPERATOR_ROLE ```solidity -function latestTimestamp() public view returns (uint256) +bytes32 PL_USD_MINT_OPERATOR_ROLE ``` -### latestRound +actor that can mint plUSD + +### PL_USD_BURN_OPERATOR_ROLE ```solidity -function latestRound() public view returns (uint256) +bytes32 PL_USD_BURN_OPERATOR_ROLE ``` -### getAnswer +actor that can burn plUSD + +### PL_USD_PAUSE_OPERATOR_ROLE ```solidity -function getAnswer(uint256) public view returns (int256) +bytes32 PL_USD_PAUSE_OPERATOR_ROLE ``` -### getTimestamp +actor that can pause plUSD + +### _getNameSymbol ```solidity -function getTimestamp(uint256) external view returns (uint256) +function _getNameSymbol() internal pure returns (string, string) ``` -### getRoundData +_returns name and symbol of the token_ -```solidity -function getRoundData(uint80) external view returns (uint80, int256, uint256, uint256, uint80) -``` +#### Return Values -### latestRoundData +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +function _minterRole() internal pure returns (bytes32) ``` -## IStakedUSDe +_AC role, owner of which can mint plUSD token_ -### convertToAssets +### _burnerRole ```solidity -function convertToAssets(uint256 shares) external view returns (uint256) +function _burnerRole() internal pure returns (bytes32) ``` -## StakedUSDeAdapter - -example https://etherscan.io/address/0x9D39A5DE30e57443BfF2A8307A4256c8797A3497 +_AC role, owner of which can burn plUSD token_ -### stakedUSDe +### _pauserRole ```solidity -contract IStakedUSDe stakedUSDe +function _pauserRole() internal pure returns (bytes32) ``` -### constructor +_AC role, owner of which can pause plUSD token_ -```solidity -constructor(address _stakedUSDe) public -``` +## SLInjCustomAggregatorFeed -### decimals +AggregatorV3 compatible feed for sLINJ, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -function decimals() external pure returns (uint8) +function feedAdminRole() public pure returns (bytes32) ``` -### description +_describes a role, owner of which can update prices in this feed_ -```solidity -function description() public pure returns (string) -``` +#### Return Values -### version +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -function version() public pure returns (uint256) -``` +## SLInjDataFeed -### latestAnswer +DataFeed for sLINJ product + +### feedAdminRole ```solidity -function latestAnswer() public view virtual returns (int256) +function feedAdminRole() public pure returns (bytes32) ``` -### latestTimestamp +_describes a role, owner of which can manage this feed_ -```solidity -function latestTimestamp() public view returns (uint256) -``` +#### Return Values -### latestRound +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -function latestRound() public view returns (uint256) -``` +## sLINJ -### getAnswer +### SL_INJ_MINT_OPERATOR_ROLE ```solidity -function getAnswer(uint256) public view returns (int256) +bytes32 SL_INJ_MINT_OPERATOR_ROLE ``` -### getTimestamp +actor that can mint sLINJ + +### SL_INJ_BURN_OPERATOR_ROLE ```solidity -function getTimestamp(uint256) external view returns (uint256) +bytes32 SL_INJ_BURN_OPERATOR_ROLE ``` -### getRoundData +actor that can burn sLINJ + +### SL_INJ_PAUSE_OPERATOR_ROLE ```solidity -function getRoundData(uint80) external view returns (uint80, int256, uint256, uint256, uint80) +bytes32 SL_INJ_PAUSE_OPERATOR_ROLE ``` -### latestRoundData +actor that can pause sLINJ + +### _getNameSymbol ```solidity -function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +function _getNameSymbol() internal pure returns (string, string) ``` -## StorkChainlinkAdapter +_returns name and symbol of the token_ -### TIMESTAMP_DIVIDER +#### Return Values -```solidity -uint256 TIMESTAMP_DIVIDER -``` +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### priceId +### _minterRole ```solidity -bytes32 priceId +function _minterRole() internal pure returns (bytes32) ``` -### stork +_AC role, owner of which can mint sLINJ token_ + +### _burnerRole ```solidity -contract IStorkTemporalNumericValueUnsafeGetter stork +function _burnerRole() internal pure returns (bytes32) ``` -### constructor +_AC role, owner of which can burn sLINJ token_ + +### _pauserRole ```solidity -constructor(address _stork, bytes32 _priceId) public +function _pauserRole() internal pure returns (bytes32) ``` -### decimals +_AC role, owner of which can pause sLINJ token_ -```solidity -function decimals() external pure returns (uint8) -``` +## SplUsdCustomAggregatorFeed -### description +AggregatorV3 compatible feed for splUSD, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -function description() public pure returns (string) +function feedAdminRole() public pure returns (bytes32) ``` -### version +_describes a role, owner of which can update prices in this feed_ -```solidity -function version() public pure returns (uint256) -``` +#### Return Values -### latestAnswer +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -function latestAnswer() public view virtual returns (int256) -``` +## SplUsdDataFeed -### latestTimestamp +DataFeed for splUSD product + +### feedAdminRole ```solidity -function latestTimestamp() public view returns (uint256) +function feedAdminRole() public pure returns (bytes32) ``` -### latestRound +_describes a role, owner of which can manage this feed_ -```solidity -function latestRound() public view returns (uint256) -``` +#### Return Values -### getAnswer +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -function getAnswer(uint256) public view returns (int256) -``` +## splUSD -### getTimestamp +### SPL_USD_MINT_OPERATOR_ROLE ```solidity -function getTimestamp(uint256) external view returns (uint256) +bytes32 SPL_USD_MINT_OPERATOR_ROLE ``` -### getRoundData +actor that can mint splUSD + +### SPL_USD_BURN_OPERATOR_ROLE ```solidity -function getRoundData(uint80 _roundId) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +bytes32 SPL_USD_BURN_OPERATOR_ROLE ``` -### latestRoundData +actor that can burn splUSD + +### SPL_USD_PAUSE_OPERATOR_ROLE ```solidity -function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +bytes32 SPL_USD_PAUSE_OPERATOR_ROLE ``` -## IStorkTemporalNumericValueUnsafeGetter +actor that can pause splUSD -### getTemporalNumericValueUnsafeV1 +### _getNameSymbol ```solidity -function getTemporalNumericValueUnsafeV1(bytes32 id) external view returns (struct StorkStructs.TemporalNumericValue value) +function _getNameSymbol() internal pure returns (string, string) ``` -## StorkStructs +_returns name and symbol of the token_ -### TemporalNumericValue +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -struct TemporalNumericValue { - uint64 timestampNs; - int192 quantizedValue; -} +function _minterRole() internal pure returns (bytes32) ``` -## TransparentUpgradeableProxyCopy +_AC role, owner of which can mint splUSD token_ -### constructor +### _burnerRole ```solidity -constructor(address _logic, address admin_, bytes _data) public payable +function _burnerRole() internal pure returns (bytes32) ``` -## IWrappedEEth +_AC role, owner of which can burn splUSD token_ -### getRate +### _pauserRole ```solidity -function getRate() external view returns (uint256) +function _pauserRole() internal pure returns (bytes32) ``` -## WrappedEEthAdapter +_AC role, owner of which can pause splUSD token_ -example https://etherscan.io/address/0xcd5fe23c85820f7b72d0926fc9b05b43e359b7ee +## TBtcCustomAggregatorFeed -### wrappedEEth +AggregatorV3 compatible feed for tBTC, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -contract IWrappedEEth wrappedEEth +function feedAdminRole() public pure returns (bytes32) ``` -### constructor +_describes a role, owner of which can update prices in this feed_ -```solidity -constructor(address _wrappedEEth) public -``` +#### Return Values -### decimals +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -function decimals() external pure returns (uint8) -``` +## TBtcDataFeed -### description +DataFeed for tBTC product + +### feedAdminRole ```solidity -function description() public pure returns (string) +function feedAdminRole() public pure returns (bytes32) ``` -### version +_describes a role, owner of which can manage this feed_ -```solidity -function version() public pure returns (uint256) -``` +#### Return Values -### latestAnswer +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## tBTC + +### T_BTC_MINT_OPERATOR_ROLE ```solidity -function latestAnswer() public view virtual returns (int256) +bytes32 T_BTC_MINT_OPERATOR_ROLE ``` -### latestTimestamp +actor that can mint tBTC + +### T_BTC_BURN_OPERATOR_ROLE ```solidity -function latestTimestamp() public view returns (uint256) +bytes32 T_BTC_BURN_OPERATOR_ROLE ``` -### latestRound +actor that can burn tBTC + +### T_BTC_PAUSE_OPERATOR_ROLE ```solidity -function latestRound() public view returns (uint256) +bytes32 T_BTC_PAUSE_OPERATOR_ROLE ``` -### getAnswer +actor that can pause tBTC + +### _getNameSymbol ```solidity -function getAnswer(uint256) public view returns (int256) +function _getNameSymbol() internal pure returns (string, string) ``` -### getTimestamp +_returns name and symbol of the token_ -```solidity -function getTimestamp(uint256) external view returns (uint256) -``` +#### Return Values -### getRoundData +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -function getRoundData(uint80) external view returns (uint80, int256, uint256, uint256, uint80) +function _minterRole() internal pure returns (bytes32) ``` -### latestRoundData +_AC role, owner of which can mint tBTC token_ + +### _burnerRole ```solidity -function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +function _burnerRole() internal pure returns (bytes32) ``` -## IWstEth +_AC role, owner of which can burn tBTC token_ -### stEthPerToken +### _pauserRole ```solidity -function stEthPerToken() external view returns (uint256) +function _pauserRole() internal pure returns (bytes32) ``` -## WstEthAdapter +_AC role, owner of which can pause tBTC token_ + +## TEthCustomAggregatorFeed -example https://etherscan.io/address/0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0 +AggregatorV3 compatible feed for tETH, +where price is submitted manually by feed admins -### wstEth +### feedAdminRole ```solidity -contract IWstEth wstEth +function feedAdminRole() public pure returns (bytes32) ``` -### constructor +_describes a role, owner of which can update prices in this feed_ -```solidity -constructor(address _wstEth) public -``` +#### Return Values -### decimals +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -function decimals() external pure returns (uint8) -``` +## TEthDataFeed -### description +DataFeed for tETH product + +### feedAdminRole ```solidity -function description() public pure returns (string) +function feedAdminRole() public pure returns (bytes32) ``` -### version +_describes a role, owner of which can manage this feed_ -```solidity -function version() public pure returns (uint256) -``` +#### Return Values -### latestAnswer +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -function latestAnswer() public view virtual returns (int256) -``` +## tETH -### latestTimestamp +### T_ETH_MINT_OPERATOR_ROLE ```solidity -function latestTimestamp() public view returns (uint256) +bytes32 T_ETH_MINT_OPERATOR_ROLE ``` -### latestRound +actor that can mint tETH + +### T_ETH_BURN_OPERATOR_ROLE ```solidity -function latestRound() public view returns (uint256) +bytes32 T_ETH_BURN_OPERATOR_ROLE ``` -### getAnswer +actor that can burn tETH + +### T_ETH_PAUSE_OPERATOR_ROLE ```solidity -function getAnswer(uint256) public view returns (int256) +bytes32 T_ETH_PAUSE_OPERATOR_ROLE ``` -### getTimestamp +actor that can pause tETH + +### _getNameSymbol ```solidity -function getTimestamp(uint256) external view returns (uint256) +function _getNameSymbol() internal pure returns (string, string) ``` -### getRoundData +_returns name and symbol of the token_ -```solidity -function getRoundData(uint80) external view returns (uint80, int256, uint256, uint256, uint80) -``` +#### Return Values -### latestRoundData +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +function _minterRole() internal pure returns (bytes32) ``` -## AggregatorV3DeprecatedMock +_AC role, owner of which can mint tETH token_ -### decimals +### _burnerRole ```solidity -function decimals() external view returns (uint8) +function _burnerRole() internal pure returns (bytes32) ``` -### description +_AC role, owner of which can burn tETH token_ + +### _pauserRole ```solidity -function description() external view returns (string) +function _pauserRole() internal pure returns (bytes32) ``` -### version +_AC role, owner of which can pause tETH token_ -```solidity -function version() external view returns (uint256) -``` +## TUsdeCustomAggregatorFeed -### setRoundData +AggregatorV3 compatible feed for tUSDe, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -function setRoundData(int256 _data) external +function feedAdminRole() public pure returns (bytes32) ``` -### getRoundData +_describes a role, owner of which can update prices in this feed_ -```solidity -function getRoundData(uint80 _roundId) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) -``` +#### Return Values -### latestRoundData +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) -``` +## TUsdeDataFeed -## AggregatorV3Mock +DataFeed for tUSDe product -### decimals +### feedAdminRole ```solidity -function decimals() external view returns (uint8) +function feedAdminRole() public pure returns (bytes32) ``` -### description +_describes a role, owner of which can manage this feed_ -```solidity -function description() external view returns (string) -``` +#### Return Values -### version +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -```solidity -function version() external view returns (uint256) -``` +## tUSDe -### setRoundData +### T_USDE_MINT_OPERATOR_ROLE ```solidity -function setRoundData(int256 _data) external +bytes32 T_USDE_MINT_OPERATOR_ROLE ``` -### getRoundData +actor that can mint tUSDe + +### T_USDE_BURN_OPERATOR_ROLE ```solidity -function getRoundData(uint80 _roundId) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +bytes32 T_USDE_BURN_OPERATOR_ROLE ``` -### latestRoundData +actor that can burn tUSDe + +### T_USDE_PAUSE_OPERATOR_ROLE ```solidity -function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +bytes32 T_USDE_PAUSE_OPERATOR_ROLE ``` -## AggregatorV3UnhealthyMock +actor that can pause tUSDe -### decimals +### _getNameSymbol ```solidity -function decimals() external view returns (uint8) +function _getNameSymbol() internal pure returns (string, string) ``` -### description +_returns name and symbol of the token_ -```solidity -function description() external view returns (string) -``` +#### Return Values -### version +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -function version() external view returns (uint256) +function _minterRole() internal pure returns (bytes32) ``` -### setRoundData +_AC role, owner of which can mint tUSDe token_ + +### _burnerRole ```solidity -function setRoundData(int256 _data) external +function _burnerRole() internal pure returns (bytes32) ``` -### getRoundData +_AC role, owner of which can burn tUSDe token_ + +### _pauserRole ```solidity -function getRoundData(uint80 _roundId) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) +function _pauserRole() internal pure returns (bytes32) ``` -### latestRoundData +_AC role, owner of which can pause tUSDe token_ -```solidity -function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) -``` +## TacTonCustomAggregatorFeed -## ERC20Mock +AggregatorV3 compatible feed for tacTON, +where price is submitted manually by feed admins -### constructor +### feedAdminRole ```solidity -constructor(uint8 decimals_) public +function feedAdminRole() public pure returns (bytes32) ``` -### mint +_describes a role, owner of which can update prices in this feed_ -```solidity -function mint(address to, uint256 amount) external -``` +#### Return Values -### decimals +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## TacTonDataFeed + +DataFeed for tacTON product + +### feedAdminRole ```solidity -function decimals() public view returns (uint8) +function feedAdminRole() public pure returns (bytes32) ``` -_Returns the number of decimals used to get its user representation. -For example, if `decimals` equals `2`, a balance of `505` tokens should -be displayed to a user as `5.05` (`505 / 10 ** 2`). +_describes a role, owner of which can manage this feed_ -Tokens usually opt for a value of 18, imitating the relationship between -Ether and Wei. This is the value {ERC20} uses, unless this function is -overridden; +#### Return Values -NOTE: This information is only used for _display_ purposes: it in -no way affects any of the arithmetic of the contract, including -{IERC20-balanceOf} and {IERC20-transfer}._ +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | -## ERC20MockWithName +## tacTON -### constructor +### TAC_TON_MINT_OPERATOR_ROLE ```solidity -constructor(uint8 decimals_, string name, string symb) public +bytes32 TAC_TON_MINT_OPERATOR_ROLE ``` -### mint - -```solidity -function mint(address to, uint256 amount) external -``` +actor that can mint tacTON -### decimals +### TAC_TON_BURN_OPERATOR_ROLE ```solidity -function decimals() public view returns (uint8) +bytes32 TAC_TON_BURN_OPERATOR_ROLE ``` -_Returns the number of decimals used to get its user representation. -For example, if `decimals` equals `2`, a balance of `505` tokens should -be displayed to a user as `5.05` (`505 / 10 ** 2`). - -Tokens usually opt for a value of 18, imitating the relationship between -Ether and Wei. This is the value {ERC20} uses, unless this function is -overridden; +actor that can burn tacTON -NOTE: This information is only used for _display_ purposes: it in -no way affects any of the arithmetic of the contract, including -{IERC20-balanceOf} and {IERC20-transfer}._ +### TAC_TON_PAUSE_OPERATOR_ROLE -## RedemptionTest +```solidity +bytes32 TAC_TON_PAUSE_OPERATOR_ROLE +``` -### asset +actor that can pause tacTON + +### _getNameSymbol ```solidity -address asset +function _getNameSymbol() internal pure returns (string, string) ``` -The asset being redeemed. +_returns name and symbol of the token_ #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | -### liquidity +### _minterRole ```solidity -address liquidity +function _minterRole() internal pure returns (bytes32) ``` -The liquidity token that the asset is being redeemed for. +_AC role, owner of which can mint tacTON token_ -#### Return Values +### _burnerRole -| Name | Type | Description | -| ---- | ---- | ----------- | +```solidity +function _burnerRole() internal pure returns (bytes32) +``` -### constructor +_AC role, owner of which can burn tacTON token_ + +### _pauserRole ```solidity -constructor(address _asset, address _liquidity) public +function _pauserRole() internal pure returns (bytes32) ``` -### settlement +_AC role, owner of which can pause tacTON token_ + +## WNlpCustomAggregatorFeed + +AggregatorV3 compatible feed for wNLP, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -function settlement() external view returns (address) +function feedAdminRole() public pure returns (bytes32) ``` -The settlement contract address. +_describes a role, owner of which can update prices in this feed_ #### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| [0] | address | The address of the settlement contract. | +| [0] | bytes32 | role descriptor | -### redeem +## WNlpDataFeed + +DataFeed for wNLP product + +### feedAdminRole ```solidity -function redeem(uint256 amount) external +function feedAdminRole() public pure returns (bytes32) ``` -Redeems an amount of asset for liquidity +_describes a role, owner of which can manage this feed_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| amount | uint256 | The amount of the asset token to redeem | +| [0] | bytes32 | role descriptor | -## USTBRedemptionMock +## wNLP -### USDC_DECIMALS +### W_NLP_MINT_OPERATOR_ROLE ```solidity -uint256 USDC_DECIMALS +bytes32 W_NLP_MINT_OPERATOR_ROLE ``` -### USDC_PRECISION +actor that can mint wNLP + +### W_NLP_BURN_OPERATOR_ROLE ```solidity -uint256 USDC_PRECISION +bytes32 W_NLP_BURN_OPERATOR_ROLE ``` -### SUPERSTATE_TOKEN_DECIMALS +actor that can burn wNLP + +### W_NLP_PAUSE_OPERATOR_ROLE ```solidity -uint256 SUPERSTATE_TOKEN_DECIMALS +bytes32 W_NLP_PAUSE_OPERATOR_ROLE ``` -### SUPERSTATE_TOKEN_PRECISION +actor that can pause wNLP + +### _getNameSymbol ```solidity -uint256 SUPERSTATE_TOKEN_PRECISION +function _getNameSymbol() internal pure returns (string, string) ``` -### FEE_DENOMINATOR +_returns name and symbol of the token_ -```solidity -uint256 FEE_DENOMINATOR -``` +#### Return Values -### CHAINLINK_FEED_PRECISION +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -uint256 CHAINLINK_FEED_PRECISION +function _minterRole() internal pure returns (bytes32) ``` -### SUPERSTATE_TOKEN +_AC role, owner of which can mint wNLP token_ + +### _burnerRole ```solidity -contract IERC20 SUPERSTATE_TOKEN +function _burnerRole() internal pure returns (bytes32) ``` -### USDC +_AC role, owner of which can burn wNLP token_ + +### _pauserRole ```solidity -contract IERC20 USDC +function _pauserRole() internal pure returns (bytes32) ``` -### redemptionFee +_AC role, owner of which can pause wNLP token_ + +## WVLPCustomAggregatorFeed + +AggregatorV3 compatible feed for wVLP, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -uint256 redemptionFee +function feedAdminRole() public pure returns (bytes32) ``` -### _maxUstbRedemptionAmount +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## WVLPDataFeed + +DataFeed for wVLP product + +### feedAdminRole ```solidity -uint256 _maxUstbRedemptionAmount +function feedAdminRole() public pure returns (bytes32) ``` -### constructor +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## wVLP + +### W_VLP_MINT_OPERATOR_ROLE ```solidity -constructor(address ustbToken, address usdcToken) public +bytes32 W_VLP_MINT_OPERATOR_ROLE ``` -### calculateFee +actor that can mint wVLP + +### W_VLP_BURN_OPERATOR_ROLE ```solidity -function calculateFee(uint256 amount) public view returns (uint256) +bytes32 W_VLP_BURN_OPERATOR_ROLE ``` -### calculateUstbIn +actor that can burn wVLP + +### W_VLP_PAUSE_OPERATOR_ROLE ```solidity -function calculateUstbIn(uint256 usdcOutAmount) public view returns (uint256 ustbInAmount, uint256 usdPerUstbChainlinkRaw) +bytes32 W_VLP_PAUSE_OPERATOR_ROLE ``` -### calculateUsdcOut +actor that can pause wVLP + +### _getNameSymbol ```solidity -function calculateUsdcOut(uint256 superstateTokenInAmount) external view returns (uint256 usdcOutAmountAfterFee, uint256 usdPerUstbChainlinkRaw) +function _getNameSymbol() internal pure returns (string, string) ``` -### _calculateUsdcOut +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -function _calculateUsdcOut(uint256 superstateTokenInAmount) internal view returns (uint256 usdcOutAmountAfterFee, uint256 usdcOutAmountBeforeFee, uint256 usdPerUstbChainlinkRaw) +function _minterRole() internal pure returns (bytes32) ``` -### maxUstbRedemptionAmount +_AC role, owner of which can mint wVLP token_ + +### _burnerRole ```solidity -function maxUstbRedemptionAmount() external view returns (uint256 superstateTokenAmount, uint256 usdPerUstbChainlinkRaw) +function _burnerRole() internal pure returns (bytes32) ``` -### redeem +_AC role, owner of which can burn wVLP token_ + +### _pauserRole ```solidity -function redeem(uint256 superstateTokenInAmount) external +function _pauserRole() internal pure returns (bytes32) ``` -### redeem +_AC role, owner of which can pause wVLP token_ + +## WeEurCustomAggregatorFeed + +AggregatorV3 compatible feed for weEUR, +where price is submitted manually by feed admins + +### feedAdminRole ```solidity -function redeem(address to, uint256 superstateTokenInAmount) external +function feedAdminRole() public pure returns (bytes32) ``` -### _redeem +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## WeEurDataFeed + +DataFeed for weEUR product + +### feedAdminRole ```solidity -function _redeem(address to, uint256 superstateTokenInAmount) internal +function feedAdminRole() public pure returns (bytes32) ``` -### withdraw +_describes a role, owner of which can manage this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +## weEUR + +### WE_EUR_MINT_OPERATOR_ROLE ```solidity -function withdraw(address _token, address to, uint256 amount) external +bytes32 WE_EUR_MINT_OPERATOR_ROLE ``` -### _getChainlinkPrice +actor that can mint weEUR + +### WE_EUR_BURN_OPERATOR_ROLE ```solidity -function _getChainlinkPrice() internal view returns (bool _isBadData, uint256 _updatedAt, uint256 _price) +bytes32 WE_EUR_BURN_OPERATOR_ROLE ``` -### _requireNotPaused +actor that can burn weEUR + +### WE_EUR_PAUSE_OPERATOR_ROLE ```solidity -function _requireNotPaused() internal view +bytes32 WE_EUR_PAUSE_OPERATOR_ROLE ``` -### setRedemptionFee +actor that can pause weEUR + +### _getNameSymbol ```solidity -function setRedemptionFee(uint256 fee) external +function _getNameSymbol() internal pure returns (string, string) ``` -### setChainlinkData +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole ```solidity -function setChainlinkData(uint256 price, bool isBadData) external +function _minterRole() internal pure returns (bytes32) ``` -### setPaused +_AC role, owner of which can mint weEUR token_ + +### _burnerRole ```solidity -function setPaused(bool paused) external +function _burnerRole() internal pure returns (bytes32) ``` -### setMaxUstbRedemptionAmount +_AC role, owner of which can burn weEUR token_ + +### _pauserRole ```solidity -function setMaxUstbRedemptionAmount(uint256 maxUstbRedemptionAmount_) external +function _pauserRole() internal pure returns (bytes32) ``` -## TBtcCustomAggregatorFeed +_AC role, owner of which can pause weEUR token_ -AggregatorV3 compatible feed for tBTC, +## ZeroGBtcvCustomAggregatorFeed + +AggregatorV3 compatible feed for zeroGBTCV, where price is submitted manually by feed admins ### feedAdminRole @@ -9019,9 +22078,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## TBtcDataFeed +## ZeroGBtcvDataFeed -DataFeed for tBTC product +DataFeed for zeroGBTCV product ### feedAdminRole @@ -9037,45 +22096,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## tBTC +## zeroGBTCV -### T_BTC_MINT_OPERATOR_ROLE +### ZEROG_BTCV_MINT_OPERATOR_ROLE ```solidity -bytes32 T_BTC_MINT_OPERATOR_ROLE +bytes32 ZEROG_BTCV_MINT_OPERATOR_ROLE ``` -actor that can mint tBTC +actor that can mint zeroGBTCV -### T_BTC_BURN_OPERATOR_ROLE +### ZEROG_BTCV_BURN_OPERATOR_ROLE ```solidity -bytes32 T_BTC_BURN_OPERATOR_ROLE +bytes32 ZEROG_BTCV_BURN_OPERATOR_ROLE ``` -actor that can burn tBTC +actor that can burn zeroGBTCV -### T_BTC_PAUSE_OPERATOR_ROLE +### ZEROG_BTCV_PAUSE_OPERATOR_ROLE ```solidity -bytes32 T_BTC_PAUSE_OPERATOR_ROLE +bytes32 ZEROG_BTCV_PAUSE_OPERATOR_ROLE ``` -actor that can pause tBTC +actor that can pause zeroGBTCV -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControl contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -9083,7 +22143,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint tBTC token_ +_AC role, owner of which can mint zeroGBTCV token_ ### _burnerRole @@ -9091,7 +22151,7 @@ _AC role, owner of which can mint tBTC token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn tBTC token_ +_AC role, owner of which can burn zeroGBTCV token_ ### _pauserRole @@ -9099,11 +22159,11 @@ _AC role, owner of which can burn tBTC token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause tBTC token_ +_AC role, owner of which can pause zeroGBTCV token_ -## TEthCustomAggregatorFeed +## ZeroGEthvCustomAggregatorFeed -AggregatorV3 compatible feed for tETH, +AggregatorV3 compatible feed for zeroGETHV, where price is submitted manually by feed admins ### feedAdminRole @@ -9120,9 +22180,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## TEthDataFeed +## ZeroGEthvDataFeed -DataFeed for tETH product +DataFeed for zeroGETHV product ### feedAdminRole @@ -9138,45 +22198,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## tETH +## zeroGETHV -### T_ETH_MINT_OPERATOR_ROLE +### ZEROG_ETHV_MINT_OPERATOR_ROLE ```solidity -bytes32 T_ETH_MINT_OPERATOR_ROLE +bytes32 ZEROG_ETHV_MINT_OPERATOR_ROLE ``` -actor that can mint tETH +actor that can mint zeroGETHV -### T_ETH_BURN_OPERATOR_ROLE +### ZEROG_ETHV_BURN_OPERATOR_ROLE ```solidity -bytes32 T_ETH_BURN_OPERATOR_ROLE +bytes32 ZEROG_ETHV_BURN_OPERATOR_ROLE ``` -actor that can burn tETH +actor that can burn zeroGETHV -### T_ETH_PAUSE_OPERATOR_ROLE +### ZEROG_ETHV_PAUSE_OPERATOR_ROLE ```solidity -bytes32 T_ETH_PAUSE_OPERATOR_ROLE +bytes32 ZEROG_ETHV_PAUSE_OPERATOR_ROLE ``` -actor that can pause tETH +actor that can pause zeroGETHV -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControl contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -9184,7 +22245,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint tETH token_ +_AC role, owner of which can mint zeroGETHV token_ ### _burnerRole @@ -9192,7 +22253,7 @@ _AC role, owner of which can mint tETH token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn tETH token_ +_AC role, owner of which can burn zeroGETHV token_ ### _pauserRole @@ -9200,11 +22261,11 @@ _AC role, owner of which can burn tETH token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause tETH token_ +_AC role, owner of which can pause zeroGETHV token_ -## TUsdeCustomAggregatorFeed +## ZeroGUsdvCustomAggregatorFeed -AggregatorV3 compatible feed for tUSDe, +AggregatorV3 compatible feed for zeroGUSDV, where price is submitted manually by feed admins ### feedAdminRole @@ -9221,9 +22282,9 @@ _describes a role, owner of which can update prices in this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## TUsdeDataFeed +## ZeroGUsdvDataFeed -DataFeed for tUSDe product +DataFeed for zeroGUSDV product ### feedAdminRole @@ -9239,45 +22300,46 @@ _describes a role, owner of which can manage this feed_ | ---- | ---- | ----------- | | [0] | bytes32 | role descriptor | -## tUSDe +## zeroGUSDV -### T_USDE_MINT_OPERATOR_ROLE +### ZEROG_USDV_MINT_OPERATOR_ROLE ```solidity -bytes32 T_USDE_MINT_OPERATOR_ROLE +bytes32 ZEROG_USDV_MINT_OPERATOR_ROLE ``` -actor that can mint tUSDe +actor that can mint zeroGUSDV -### T_USDE_BURN_OPERATOR_ROLE +### ZEROG_USDV_BURN_OPERATOR_ROLE ```solidity -bytes32 T_USDE_BURN_OPERATOR_ROLE +bytes32 ZEROG_USDV_BURN_OPERATOR_ROLE ``` -actor that can burn tUSDe +actor that can burn zeroGUSDV -### T_USDE_PAUSE_OPERATOR_ROLE +### ZEROG_USDV_PAUSE_OPERATOR_ROLE ```solidity -bytes32 T_USDE_PAUSE_OPERATOR_ROLE +bytes32 ZEROG_USDV_PAUSE_OPERATOR_ROLE ``` -actor that can pause tUSDe +actor that can pause zeroGUSDV -### initialize +### _getNameSymbol ```solidity -function initialize(address _accessControl) external +function _getNameSymbol() internal pure returns (string, string) ``` -upgradeable pattern contract`s initializer +_returns name and symbol of the token_ -#### Parameters +#### Return Values | Name | Type | Description | | ---- | ---- | ----------- | -| _accessControl | address | address of MidasAccessControl contract | +| [0] | string | name of the token | +| [1] | string | symbol of the token | ### _minterRole @@ -9285,7 +22347,7 @@ upgradeable pattern contract`s initializer function _minterRole() internal pure returns (bytes32) ``` -_AC role, owner of which can mint tUSDe token_ +_AC role, owner of which can mint zeroGUSDV token_ ### _burnerRole @@ -9293,7 +22355,7 @@ _AC role, owner of which can mint tUSDe token_ function _burnerRole() internal pure returns (bytes32) ``` -_AC role, owner of which can burn tUSDe token_ +_AC role, owner of which can burn zeroGUSDV token_ ### _pauserRole @@ -9301,7 +22363,7 @@ _AC role, owner of which can burn tUSDe token_ function _pauserRole() internal pure returns (bytes32) ``` -_AC role, owner of which can pause tUSDe token_ +_AC role, owner of which can pause zeroGUSDV token_ ## BlacklistableTester @@ -9342,6 +22404,21 @@ through proxies. Emits an {Initialized} event the first time it is successfully executed._ +## CompositeDataFeedTest + +### _disableInitializers + +```solidity +function _disableInitializers() internal +``` + +_Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. +Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized +to any version. It is recommended to use this to lock implementation contracts that are designed to be called +through proxies. + +Emits an {Initialized} event the first time it is successfully executed._ + ## CustomAggregatorV3CompatibleFeedDiscountedTester ### constructor @@ -9356,6 +22433,53 @@ constructor(address _underlyingFeed, uint256 _discountPercentage) public function getDiscountedAnswer(int256 _answer) public view returns (int256) ``` +## CustomAggregatorV3CompatibleFeedGrowthTester + +### CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE + +```solidity +bytes32 CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE +``` + +### _disableInitializers + +```solidity +function _disableInitializers() internal +``` + +_Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. +Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized +to any version. It is recommended to use this to lock implementation contracts that are designed to be called +through proxies. + +Emits an {Initialized} event the first time it is successfully executed._ + +### feedAdminRole + +```solidity +function feedAdminRole() public pure returns (bytes32) +``` + +_describes a role, owner of which can update prices in this feed_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role descriptor | + +### setMaxAnswerDeviation + +```solidity +function setMaxAnswerDeviation(uint256 _deviation) public +``` + +### getDeviation + +```solidity +function getDeviation(int256 _lastPrice, int256 _newPrice, bool _validateOnlyUp) public pure returns (uint256) +``` + ## CustomAggregatorV3CompatibleFeedTester ### CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE @@ -9452,12 +22576,41 @@ function initializeUnchainedWithoutInitializer() external function onlyGreenlistedTester(address account) external ``` -### onlyGreenlistTogglerTester +### validateGreenlistableAdminAccess + +```solidity +function validateGreenlistableAdminAccess(address account) external view +``` + +### _disableInitializers + +```solidity +function _disableInitializers() internal +``` + +_Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. +Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized +to any version. It is recommended to use this to lock implementation contracts that are designed to be called +through proxies. + +Emits an {Initialized} event the first time it is successfully executed._ + +### _validateGreenlistableAdminAccess + +```solidity +function _validateGreenlistableAdminAccess(address account) internal view +``` + +_checks that a given `account` has access to greenlistable functions_ + +### greenlistAdminRole ```solidity -function onlyGreenlistTogglerTester(address account) external view +function greenlistAdminRole() public view virtual returns (bytes32) ``` +## ManageableVaultTester + ### _disableInitializers ```solidity @@ -9471,6 +22624,32 @@ through proxies. Emits an {Initialized} event the first time it is successfully executed._ +### initialize + +```solidity +function initialize(struct CommonVaultInitParams _commonVaultInitParams, struct CommonVaultV2InitParams _commonVaultV2InitParams) external +``` + +### initializeWithoutInitializer + +```solidity +function initializeWithoutInitializer(struct CommonVaultInitParams _commonVaultInitParams, struct CommonVaultV2InitParams _commonVaultV2InitParams) external +``` + +### vaultRole + +```solidity +function vaultRole() public view virtual returns (bytes32) +``` + +AC role of vault administrator + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role bytes32 role | + ## MidasAccessControlTest ### _disableInitializers @@ -9500,14 +22679,26 @@ function initialize(address _accessControl) external function initializeWithoutInitializer(address _accessControl) external ``` +### _validatePauseAdminAccess + +```solidity +function _validatePauseAdminAccess(address account) internal view +``` + +_validates that the caller has access to pause functions_ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| account | address | account address | + ### pauseAdminRole ```solidity function pauseAdminRole() public view returns (bytes32) ``` -_virtual function to determine pauseAdmin role_ - ### _disableInitializers ```solidity @@ -9604,15 +22795,13 @@ function onlyNotSanctionedTester(address user) public function sanctionsListAdminRole() public pure returns (bytes32) ``` -AC role of sanctions list admin - -_address that have this role can use `setSanctionsList`_ +### _validateSanctionListAdminAccess -#### Return Values +```solidity +function _validateSanctionListAdminAccess(address account) internal view +``` -| Name | Type | Description | -| ---- | ---- | ----------- | -| [0] | bytes32 | role bytes32 role | +_validates that the caller has access to sanctions list functions_ ### _disableInitializers @@ -9642,3 +22831,167 @@ through proxies. Emits an {Initialized} event the first time it is successfully executed._ +## mTokenPermissionedTest + +### M_TOKEN_TEST_MINT_OPERATOR_ROLE + +```solidity +bytes32 M_TOKEN_TEST_MINT_OPERATOR_ROLE +``` + +### M_TOKEN_TEST_BURN_OPERATOR_ROLE + +```solidity +bytes32 M_TOKEN_TEST_BURN_OPERATOR_ROLE +``` + +### M_TOKEN_TEST_PAUSE_OPERATOR_ROLE + +```solidity +bytes32 M_TOKEN_TEST_PAUSE_OPERATOR_ROLE +``` + +### M_TOKEN_TEST_GREENLISTED_ROLE + +```solidity +bytes32 M_TOKEN_TEST_GREENLISTED_ROLE +``` + +### _disableInitializers + +```solidity +function _disableInitializers() internal +``` + +_Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. +Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized +to any version. It is recommended to use this to lock implementation contracts that are designed to be called +through proxies. + +Emits an {Initialized} event the first time it is successfully executed._ + +### _getNameSymbol + +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole + +```solidity +function _minterRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can mint mToken token_ + +### _burnerRole + +```solidity +function _burnerRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can burn mToken token_ + +### _pauserRole + +```solidity +function _pauserRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can pause mToken token_ + +### _greenlistedRole + +```solidity +function _greenlistedRole() internal pure returns (bytes32) +``` + +AC role of a greenlist + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | bytes32 | role bytes32 role | + +## mTokenTest + +### M_TOKEN_TEST_MINT_OPERATOR_ROLE + +```solidity +bytes32 M_TOKEN_TEST_MINT_OPERATOR_ROLE +``` + +### M_TOKEN_TEST_BURN_OPERATOR_ROLE + +```solidity +bytes32 M_TOKEN_TEST_BURN_OPERATOR_ROLE +``` + +### M_TOKEN_TEST_PAUSE_OPERATOR_ROLE + +```solidity +bytes32 M_TOKEN_TEST_PAUSE_OPERATOR_ROLE +``` + +### _disableInitializers + +```solidity +function _disableInitializers() internal +``` + +_Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. +Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized +to any version. It is recommended to use this to lock implementation contracts that are designed to be called +through proxies. + +Emits an {Initialized} event the first time it is successfully executed._ + +### _getNameSymbol + +```solidity +function _getNameSymbol() internal pure returns (string, string) +``` + +_returns name and symbol of the token_ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +| [0] | string | name of the token | +| [1] | string | symbol of the token | + +### _minterRole + +```solidity +function _minterRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can mint mToken token_ + +### _burnerRole + +```solidity +function _burnerRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can burn mToken token_ + +### _pauserRole + +```solidity +function _pauserRole() internal pure returns (bytes32) +``` + +_AC role, owner of which can pause mToken token_ + diff --git a/hardhat.config.ts b/hardhat.config.ts index b8098507..0464c5e2 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -14,8 +14,8 @@ import 'hardhat-deploy'; import 'hardhat-gas-reporter'; import 'solidity-coverage'; import 'hardhat-tracer'; - import './tasks'; + import { chainIds, ENV, @@ -35,20 +35,11 @@ const config: HardhatUserConfig = { solidity: { compilers: [ { - version: '0.8.9', - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - }, - }, - { - version: '0.8.22', + version: '0.8.34', settings: { optimizer: { enabled: true, - runs: 200, + runs: 1, }, }, }, diff --git a/helpers/contracts.ts b/helpers/contracts.ts index 542909be..59e4ae82 100644 --- a/helpers/contracts.ts +++ b/helpers/contracts.ts @@ -10,21 +10,24 @@ export type TokenContractNames = { rv: string; rvSwapper: string; rvMToken: string; - rvBuidl: string; rvUstb: string; rvAave: string; rvMorpho: string; - dataFeed?: string; - dataFeedComposite?: string; - dataFeedMultiply?: string; + dataFeed: string; + dataFeedComposite: string; + dataFeedMultiply: string; customAggregator?: string; customAggregatorGrowth?: string; token: string; + tokenPermissioned: string; roles: string; }; -type CommonContractNames = Omit & { +type CommonContractNames = TokenContractNames & { ac: string; + pauseManager: string; + timelockManager: string; + timelockController: string; customAggregator: string; customAggregatorAdjusted: string; layerZero: { @@ -43,9 +46,9 @@ const vaultTypeToContractNameMap: Record = { depositVaultAave: 'dvAave', depositVaultMorpho: 'dvMorpho', depositVaultMToken: 'dvMToken', - redemptionVaultBuidl: 'rvBuidl', redemptionVaultAave: 'rvAave', redemptionVaultMorpho: 'rvMorpho', + redemptionVaultBuidl: 'rvBuidl', }; export const vaultTypeToContractName = ( @@ -148,7 +151,12 @@ export const contractNamesPrefixes: Record = { export const getCommonContractNames = (): CommonContractNames => { return { ac: 'MidasAccessControl', + pauseManager: 'MidasPauseManager', + timelockManager: 'MidasTimelockManager', + timelockController: 'MidasAccessControlTimelockController', dv: 'DepositVault', + token: 'mToken', + tokenPermissioned: 'mTokenPermissioned', dvUstb: 'DepositVaultWithUSTB', dvAave: 'DepositVaultWithAave', dvMorpho: 'DepositVaultWithMorpho', @@ -156,7 +164,6 @@ export const getCommonContractNames = (): CommonContractNames => { rv: 'RedemptionVault', rvSwapper: 'RedemptionVaultWithSwapper', rvMToken: 'RedemptionVaultWithMToken', - rvBuidl: 'RedemptionVaultWIthBUIDL', rvUstb: 'RedemptionVaultWithUSTB', rvAave: 'RedemptionVaultWithAave', rvMorpho: 'RedemptionVaultWithMorpho', @@ -174,35 +181,11 @@ export const getCommonContractNames = (): CommonContractNames => { }; }; +// TODO: remove this function export const getTokenContractNames = ( - token: MTokenName, + _token: MTokenName, ): TokenContractNames => { const commonContractNames = getCommonContractNames(); - const prefix = contractNamesPrefixes[token]; - - const isMtbill = token === 'mTBILL'; - const isTac = token.startsWith('TAC'); - const tokenPrefix = isMtbill ? '' : prefix; - return { - dv: `${tokenPrefix}${commonContractNames.dv}`, - dvUstb: `${tokenPrefix}${commonContractNames.dvUstb}`, - dvAave: `${tokenPrefix}${commonContractNames.dvAave}`, - dvMorpho: `${tokenPrefix}${commonContractNames.dvMorpho}`, - dvMToken: `${tokenPrefix}${commonContractNames.dvMToken}`, - rv: `${tokenPrefix}${commonContractNames.rv}`, - rvSwapper: `${tokenPrefix}${commonContractNames.rvSwapper}`, - rvMToken: `${tokenPrefix}${commonContractNames.rvMToken}`, - rvBuidl: `${tokenPrefix}${commonContractNames.rvBuidl}`, - rvUstb: `${tokenPrefix}${commonContractNames.rvUstb}`, - rvAave: `${tokenPrefix}${commonContractNames.rvAave}`, - rvMorpho: `${tokenPrefix}${commonContractNames.rvMorpho}`, - dataFeed: isTac ? undefined : `${prefix}${commonContractNames.dataFeed}`, - customAggregator: isTac ? undefined : `${prefix}CustomAggregatorFeed`, - customAggregatorGrowth: isTac - ? undefined - : `${prefix}CustomAggregatorFeedGrowth`, - token: `${token}`, - roles: `${prefix}${commonContractNames.roles}`, - }; + return commonContractNames; }; diff --git a/helpers/roles.ts b/helpers/roles.ts index 2775c6e9..a0fd41d6 100644 --- a/helpers/roles.ts +++ b/helpers/roles.ts @@ -129,10 +129,10 @@ const getGreenlistRoleName = (token: MTokenName): string => { type TokenRoles = { minter: string; burner: string; - pauser: string; + tokenManager: string; depositVaultAdmin: string; redemptionVaultAdmin: string; - customFeedAdmin: string | null; + customFeedAdmin: string; greenlisted: string; }; @@ -142,6 +142,9 @@ type CommonRoles = { greenlistedOperator: string; blacklistedOperator: string; defaultAdmin: string; + pauseAdmin: string; + securityCouncilManager: string; + timelockOperationPauser: string; }; type IntegrationRoles = { @@ -168,10 +171,8 @@ export const getRolesNamesForToken = (token: MTokenName): TokenRoles => { return { minter: `${tokenPrefix}_MINT_OPERATOR_ROLE`, burner: `${tokenPrefix}_BURN_OPERATOR_ROLE`, - pauser: `${tokenPrefix}_PAUSE_OPERATOR_ROLE`, - customFeedAdmin: isTAC - ? null - : `${tokenPrefix}_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE`, + tokenManager: `${tokenPrefix}_TOKEN_MANAGER_ROLE`, + customFeedAdmin: `${tokenPrefix}_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE`, depositVaultAdmin: `${restPrefix}DEPOSIT_VAULT_ADMIN_ROLE`, redemptionVaultAdmin: `${restPrefix}REDEMPTION_VAULT_ADMIN_ROLE`, greenlisted: getGreenlistRoleName(token), @@ -184,6 +185,9 @@ export const getRolesNamesCommon = (): CommonRoles => { greenlistedOperator: 'GREENLIST_OPERATOR_ROLE', blacklisted: 'BLACKLISTED_ROLE', blacklistedOperator: 'BLACKLIST_OPERATOR_ROLE', + pauseAdmin: 'PAUSE_ADMIN_ROLE', + securityCouncilManager: 'SECURITY_COUNCIL_MANAGER_ROLE', + timelockOperationPauser: 'TIMELOCK_OPERATION_PAUSER_ROLE', }; }; @@ -227,6 +231,13 @@ export const getAllRoles = (): AllRoles => { greenlistedOperator: keccak256(rolesNamesCommon.greenlistedOperator), blacklisted: keccak256(rolesNamesCommon.blacklisted), blacklistedOperator: keccak256(rolesNamesCommon.blacklistedOperator), + pauseAdmin: keccak256(rolesNamesCommon.pauseAdmin), + securityCouncilManager: keccak256( + rolesNamesCommon.securityCouncilManager, + ), + timelockOperationPauser: keccak256( + rolesNamesCommon.timelockOperationPauser, + ), }, tokenRoles: Object.fromEntries( Object.keys(prefixes).map((token) => [ diff --git a/helpers/utils.ts b/helpers/utils.ts index 8ac334c1..920e980e 100644 --- a/helpers/utils.ts +++ b/helpers/utils.ts @@ -15,6 +15,37 @@ import { export const DAY = 86400; +const contractNameToPath = { + access: [ + 'MidasAccessControl', + 'MidasPauseManager', + 'MidasTimelockManager', + 'MidasAccessControlTimelockController', + ], + feeds: [ + 'CompositeDataFeed', + 'CompositeDataFeedMultiply', + 'CustomAggregatorV3CompatibleFeed', + 'CustomAggregatorV3CompatibleFeedAdjusted', + 'CustomAggregatorV3CompatibleFeedGrowth', + 'DataFeed', + ], + root: ['mToken', 'mTokenPermissioned'], +}; + +const getContractPath = (contractName: string) => { + for (const [key, value] of Object.entries(contractNameToPath)) { + if (value.includes(contractName)) { + const path = `contracts/${ + key === 'root' ? '' : key + '/' + }${contractName}.sol:${contractName}`; + return path; + } + } + + return undefined; +}; + export function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -87,11 +118,19 @@ export const getPaymentTokenOrThrow = (hre: HardhatRuntimeEnvironment) => { return paymentToken; }; -export const getActionOrThrow = (hre: HardhatRuntimeEnvironment) => { +export const upgradeActions = ['propose', 'execute'] as const; + +export const getActionOrThrow = ( + hre: HardhatRuntimeEnvironment, + validActions?: string[] | readonly string[], +) => { const action = hre.action; if (!action) { throw new Error('Action parameter not found'); } + if (validActions?.length && !validActions.includes(action)) { + throw new Error(`Invalid action: ${action}`); + } return action; }; @@ -138,20 +177,27 @@ export const logDeploy = ( export const etherscanVerify = async ( hre: HardhatRuntimeEnvironment, contractAddress: string, + contractName: string, ...constructorArguments: unknown[] ) => { const network = hre.network.name; if (network === 'localhost' || network === 'hardhat') return; - await verify(hre, contractAddress, ...constructorArguments); + await verify(hre, contractAddress, contractName, ...constructorArguments); }; export const etherscanVerifyImplementation = async ( hre: HardhatRuntimeEnvironment, proxyAddress: string, + contractName: string, ...constructorArguments: unknown[] ) => { const contractAddress = await getImplAddressFromProxy(hre, proxyAddress); - return etherscanVerify(hre, contractAddress, ...constructorArguments); + return etherscanVerify( + hre, + contractAddress, + contractName, + ...constructorArguments, + ); }; export const logDeployProxy = async ( @@ -175,11 +221,13 @@ export const logDeployProxy = async ( export const tryEtherscanVerifyImplementation = async ( hre: HardhatRuntimeEnvironment, proxyAddress: string, + contractName: string, ...constructorArguments: unknown[] ) => { return await etherscanVerifyImplementation( hre, proxyAddress, + contractName, ...constructorArguments, ) .catch((err) => { @@ -194,12 +242,14 @@ export const tryEtherscanVerifyImplementation = async ( export const verify = async ( hre: HardhatRuntimeEnvironment, contractAddress: string, + contractName: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any ...constructorArguments: any[] ) => { await hre.run('verify:verify', { address: contractAddress, constructorArguments, + contract: getContractPath(contractName), }); }; diff --git a/package.json b/package.json index 7664073e..639294fa 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,10 @@ "name": "midas-contracts", "version": "2.0.0", "description": "", - "license": "AGPL-3.0", + "license": "BUSL-1.1", "author": "RedDuck Software", "engines": { - "node": ">=22" + "node": ">=22 <23" }, "scripts": { "postinstall": "sh -c '[ \"$NODE_ENV\" = \"production\" ] || husky install'", @@ -15,10 +15,10 @@ "docgen": "hardhat docgen", "compile": "hardhat compile", "build": "run-s clean compile", - "test:unit": "hardhat test $(find test/unit -name \"*.test.ts\")", - "test:unit:parallel": "yarn test:unit --parallel", - "test:integration": "hardhat test $(find test/integration -name \"*.test.ts\")", - "test": "run-p test:unit test:integration", + "test:unit": "cross-env TS_NODE_TRANSPILE_ONLY=1 hardhat test $(find test/unit -name \"*.test.ts\")", + "test:unit:parallel": "cross-env TS_NODE_TRANSPILE_ONLY=1 hardhat test --parallel $(find test/unit -name \"*.test.ts\")", + "test:integration": "cross-env TS_NODE_TRANSPILE_ONLY=1 hardhat test $(find test/integration -name \"*.test.ts\")", + "test": "run-s test:unit test:integration", "test:parallel": "run-s test:unit:parallel test:integration", "coverage": "cross-env COVERAGE=true hardhat coverage --solcoverjs ./.solcover.json --testfiles 'test/unit/*.test.ts'", "slither": "slither . --config-file ./slither.config.json", @@ -99,6 +99,7 @@ "verify:proxy": "yarn hardhat verify-transparent-proxy", "verify:sourcify": "VERIFY_SOURCIFY=true VERIFY_ETHERSCAN=false yarn hardhat verify", "dump:roles": "yarn hh:run scripts/dump_roles.ts", + "dump:errors": "ts-node scripts/generate-errors-readme.ts", "verify:all:impl": "yarn hh:run:script scripts/verify_contracts.ts", "oz:merge-manifest": "ts-node scripts/merge-oz-manifests.ts" }, diff --git a/scripts/deploy/codegen/common/templates/aggregator.template.ts b/scripts/deploy/codegen/common/templates/aggregator.template.ts index e7ba534f..580c503d 100644 --- a/scripts/deploy/codegen/common/templates/aggregator.template.ts +++ b/scripts/deploy/codegen/common/templates/aggregator.template.ts @@ -21,8 +21,8 @@ export const getCustomAggregatorContractFromTemplate = async ( return { name: contractNames.customAggregator, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; import "../../feeds/CustomAggregatorV3CompatibleFeed.sol"; import "./${contractNames.roles}.sol"; @@ -72,8 +72,8 @@ export const getCustomAggregatorGrowthContractFromTemplate = async ( return { name: contractNames.customAggregatorGrowth, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; import "../../feeds/CustomAggregatorV3CompatibleFeedGrowth.sol"; import "./${contractNames.roles}.sol"; diff --git a/scripts/deploy/codegen/common/templates/data-feed.template.ts b/scripts/deploy/codegen/common/templates/data-feed.template.ts index a85d051e..0b8ff1e2 100644 --- a/scripts/deploy/codegen/common/templates/data-feed.template.ts +++ b/scripts/deploy/codegen/common/templates/data-feed.template.ts @@ -18,8 +18,8 @@ export const getDataFeedContractFromTemplate = async (mToken: MTokenName) => { return { name: contractNames.dataFeed, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; import "../../feeds/DataFeed.sol"; import "./${contractNames.roles}.sol"; diff --git a/scripts/deploy/codegen/common/templates/dv-aave.template.ts b/scripts/deploy/codegen/common/templates/dv-aave.template.ts index 25001389..9f06c08b 100644 --- a/scripts/deploy/codegen/common/templates/dv-aave.template.ts +++ b/scripts/deploy/codegen/common/templates/dv-aave.template.ts @@ -20,8 +20,8 @@ export const getDvAaveContractFromTemplate = async ( return { name: contractNames.dvAave, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; import "../../DepositVaultWithAave.sol"; import "./${contractNames.roles}.sol"; diff --git a/scripts/deploy/codegen/common/templates/dv-morpho.template.ts b/scripts/deploy/codegen/common/templates/dv-morpho.template.ts index edb69743..af826528 100644 --- a/scripts/deploy/codegen/common/templates/dv-morpho.template.ts +++ b/scripts/deploy/codegen/common/templates/dv-morpho.template.ts @@ -20,8 +20,8 @@ export const getDvMorphoContractFromTemplate = async ( return { name: contractNames.dvMorpho, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; import "../../DepositVaultWithMorpho.sol"; import "./${contractNames.roles}.sol"; diff --git a/scripts/deploy/codegen/common/templates/dv-mtoken.template.ts b/scripts/deploy/codegen/common/templates/dv-mtoken.template.ts index fabd91ad..3e0ae115 100644 --- a/scripts/deploy/codegen/common/templates/dv-mtoken.template.ts +++ b/scripts/deploy/codegen/common/templates/dv-mtoken.template.ts @@ -20,8 +20,8 @@ export const getDvMTokenContractFromTemplate = async ( return { name: contractNames.dvMToken, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; import "../../DepositVaultWithMToken.sol"; import "./${contractNames.roles}.sol"; diff --git a/scripts/deploy/codegen/common/templates/dv.template.ts b/scripts/deploy/codegen/common/templates/dv.template.ts index c8de0470..c2cc681e 100644 --- a/scripts/deploy/codegen/common/templates/dv.template.ts +++ b/scripts/deploy/codegen/common/templates/dv.template.ts @@ -20,8 +20,8 @@ export const getDvContractFromTemplate = async ( return { name: contractNames.dv, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; import "../../DepositVault.sol"; import "./${contractNames.roles}.sol"; diff --git a/scripts/deploy/codegen/common/templates/mtoken.template.ts b/scripts/deploy/codegen/common/templates/mtoken.template.ts index c27ce66b..9772f7d8 100644 --- a/scripts/deploy/codegen/common/templates/mtoken.template.ts +++ b/scripts/deploy/codegen/common/templates/mtoken.template.ts @@ -26,8 +26,8 @@ export const getTokenContractFromTemplate = async ( return { name: contractNames.token, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; import "../../mToken${isPermissionedMToken ? 'Permissioned' : ''}.sol"; ${isPermissionedMToken ? `import "./${contractNames.roles}.sol";` : ''} diff --git a/scripts/deploy/codegen/common/templates/rv-aave.template.ts b/scripts/deploy/codegen/common/templates/rv-aave.template.ts index 89e520ae..5cc69366 100644 --- a/scripts/deploy/codegen/common/templates/rv-aave.template.ts +++ b/scripts/deploy/codegen/common/templates/rv-aave.template.ts @@ -20,8 +20,8 @@ export const getRvAaveContractFromTemplate = async ( return { name: contractNames.rvAave, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; import "../../RedemptionVaultWithAave.sol"; import "./${contractNames.roles}.sol"; diff --git a/scripts/deploy/codegen/common/templates/rv-morpho.template.ts b/scripts/deploy/codegen/common/templates/rv-morpho.template.ts index 36fc4caa..1fe18ef3 100644 --- a/scripts/deploy/codegen/common/templates/rv-morpho.template.ts +++ b/scripts/deploy/codegen/common/templates/rv-morpho.template.ts @@ -20,8 +20,8 @@ export const getRvMorphoContractFromTemplate = async ( return { name: contractNames.rvMorpho, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; import "../../RedemptionVaultWithMorpho.sol"; import "./${contractNames.roles}.sol"; diff --git a/scripts/deploy/codegen/common/templates/rv-mtoken.template.ts b/scripts/deploy/codegen/common/templates/rv-mtoken.template.ts index 6baa4c12..ebd1820c 100644 --- a/scripts/deploy/codegen/common/templates/rv-mtoken.template.ts +++ b/scripts/deploy/codegen/common/templates/rv-mtoken.template.ts @@ -20,8 +20,8 @@ export const getRvMTokenContractFromTemplate = async ( return { name: contractNames.rvMToken, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; import "../../RedemptionVaultWithMToken.sol"; import "./${contractNames.roles}.sol"; diff --git a/scripts/deploy/codegen/common/templates/rv-swapper.template.ts b/scripts/deploy/codegen/common/templates/rv-swapper.template.ts index 07422069..9fb078fb 100644 --- a/scripts/deploy/codegen/common/templates/rv-swapper.template.ts +++ b/scripts/deploy/codegen/common/templates/rv-swapper.template.ts @@ -20,8 +20,8 @@ export const getRvSwapperContractFromTemplate = async ( return { name: contractNames.rvSwapper, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; import "../../RedemptionVaultWithSwapper.sol"; import "./${contractNames.roles}.sol"; diff --git a/scripts/deploy/codegen/common/templates/rv-ustb.template.ts b/scripts/deploy/codegen/common/templates/rv-ustb.template.ts index fb7cf6af..c4c54ded 100644 --- a/scripts/deploy/codegen/common/templates/rv-ustb.template.ts +++ b/scripts/deploy/codegen/common/templates/rv-ustb.template.ts @@ -20,8 +20,8 @@ export const getRvUstbContractFromTemplate = async ( return { name: contractNames.rvUstb, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; import "../../RedemptionVaultWithUSTB.sol"; import "./${contractNames.roles}.sol"; diff --git a/scripts/deploy/codegen/common/templates/rv.template.ts b/scripts/deploy/codegen/common/templates/rv.template.ts index b309a89b..d1b7a33c 100644 --- a/scripts/deploy/codegen/common/templates/rv.template.ts +++ b/scripts/deploy/codegen/common/templates/rv.template.ts @@ -21,8 +21,8 @@ export const getRvContractFromTemplate = async ( return { name: contractNames.rv, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; import "../../RedemptionVault.sol"; import "./${contractNames.roles}.sol"; diff --git a/scripts/deploy/codegen/common/templates/token-roles.template.ts b/scripts/deploy/codegen/common/templates/token-roles.template.ts index 1120bb96..1e831682 100644 --- a/scripts/deploy/codegen/common/templates/token-roles.template.ts +++ b/scripts/deploy/codegen/common/templates/token-roles.template.ts @@ -21,8 +21,8 @@ export const getTokenRolesContractFromTemplate = async ( return { name: contractNames.roles, content: ` - // SPDX-License-Identifier: MIT - pragma solidity 0.8.9; + // SPDX-License-Identifier: BUSL-1.1 + pragma solidity 0.8.34; /** * @title ${contractNames.roles} diff --git a/scripts/deploy/common/common-vault.ts b/scripts/deploy/common/common-vault.ts index b0eba545..52c231a6 100644 --- a/scripts/deploy/common/common-vault.ts +++ b/scripts/deploy/common/common-vault.ts @@ -42,6 +42,7 @@ export type AddFeeWaivedConfig = { mToken: MTokenName; type: VaultType; }; + value?: boolean; toWaive: ( | { // default: hre.mtoken @@ -52,7 +53,7 @@ export type AddFeeWaivedConfig = { )[]; }[]; -export const addFeeWaived = async ( +export const setFeeWaivedAccounts = async ( hre: HardhatRuntimeEnvironment, token: MTokenName, ) => { @@ -93,16 +94,21 @@ export const addFeeWaived = async ( vaultContract, feeWaiveAddress, feeWaiveLabel, + value, ) => { const waived = await vaultContract.waivedFeeRestriction(feeWaiveAddress!); - if (waived) { - console.log('Fee is already waived, skipping...', feeWaiveAddress); + if (waived === value) { + console.log( + `Fee is already ${value ? 'waived' : 'not waived'}, skipping...`, + feeWaiveAddress, + ); return; } - const tx = await vaultContract.populateTransaction.addWaivedFeeAccount( + const tx = await vaultContract.populateTransaction.setWaivedFeeAccount( feeWaiveAddress!, + value, ); const txRes = await sendAndWaitForCustomTxSign(hre, tx, { @@ -192,6 +198,7 @@ const foreachFeeWaiveAddress = async ( vaultContract: ManageableVault, feeWaiveAddress: string, feeWaiveLabel: string, + value: boolean, ) => Promise, ) => { const addresses = getCurrentAddresses(hre); @@ -222,6 +229,7 @@ const foreachFeeWaiveAddress = async ( typeof toWaive === 'string' ? toWaive : `${toWaive.mToken ?? token} ${toWaive.type}`, + vault.value ?? true, ); } } diff --git a/scripts/deploy/common/data-feed.ts b/scripts/deploy/common/data-feed.ts index 8f76f2d6..fffa924e 100644 --- a/scripts/deploy/common/data-feed.ts +++ b/scripts/deploy/common/data-feed.ts @@ -22,6 +22,7 @@ import { getCommonContractNames, getTokenContractNames, } from '../../../helpers/contracts'; +import { getAllRoles } from '../../../helpers/roles'; import { CustomAggregatorV3CompatibleFeed, CustomAggregatorV3CompatibleFeedGrowth, @@ -204,7 +205,7 @@ const setRoundData = async ( tx = await aggregator.populateTransaction.setRoundData( networkConfig.data, - networkConfig.dataTimestamp ?? currentTimestamp, + networkConfig.dataTimestamp ?? currentTimestamp - 1, networkConfig.apr, ); log = `${token} set price to ${formatUnits( @@ -483,15 +484,6 @@ export const deployPaymentTokenDataFeed = async ( const compositeConfig = networkConfig as DeployDataFeedConfigComposite; const feedType = compositeConfig.feedType; - const contractName = - feedType === 'multiply' - ? getCommonContractNames().dataFeedMultiply - : getCommonContractNames().dataFeedComposite; - - if (!contractName) { - throw new Error(`${feedType} data feed contract name is not set`); - } - if ( !tokenAddresses?.denominator?.dataFeed || !tokenAddresses?.numerator?.dataFeed @@ -504,7 +496,6 @@ export const deployPaymentTokenDataFeed = async ( hre, tokenAddresses.numerator.dataFeed, tokenAddresses.denominator.dataFeed, - contractName, compositeConfig, ); } else { @@ -512,13 +503,10 @@ export const deployPaymentTokenDataFeed = async ( hre, tokenAddresses.numerator.dataFeed, tokenAddresses.denominator.dataFeed, - contractName, compositeConfig, ); } } else { - const contractName = getCommonContractNames().dataFeed; - let aggregator: string | undefined; let config: DeployDataFeedConfigRegular; @@ -534,15 +522,17 @@ export const deployPaymentTokenDataFeed = async ( throw new Error('Incorrect params'); } - if (!contractName) { - throw new Error('Data feed contract name is not set'); - } - if (!aggregator) { throw new Error('Token config is not found or aggregator is not set'); } - await deployTokenDataFeed(hre, aggregator, contractName, config); + const roles = getAllRoles(); + await deployTokenDataFeed( + hre, + aggregator, + roles.common.defaultAdmin, + config, + ); } }; @@ -562,9 +552,11 @@ export const deployPaymentTokenCustomAggregator = async ( paymentToken ]?.customAggregator; + const roles = getAllRoles(); await deployCustomAggregator( hre, customAggregatorContractName, + roles.common.defaultAdmin, networkConfig, ); }; @@ -585,20 +577,15 @@ export const deployMTokenDataFeed = async ( throw new Error('Token config is not found or customFeed is not set'); } - const dataFeedContractName = getTokenContractNames(token).dataFeed; - - if (!dataFeedContractName) { - throw new Error('Data feed contract name is not set'); - } - if (tokenAddresses?.customFeedAdjusted) { console.log('Using single adjusted feed as aggregator for DataFeed'); } + const roles = getAllRoles(); await deployTokenDataFeed( hre, aggregator, - dataFeedContractName, + roles.tokenRoles[token].customFeedAdmin!, getDeploymentGenericConfig(hre, token, 'dataFeed'), ); }; @@ -647,16 +634,11 @@ export const deployMTokenDataFeedDv = async ( throw new Error('Token config is not found or customFeedDv is not set'); } - const dataFeedContractName = getTokenContractNames(token).dataFeed; - - if (!dataFeedContractName) { - throw new Error('Data feed contract name is not set'); - } - + const roles = getAllRoles(); await deployTokenDataFeed( hre, tokenAddresses.customFeedDv, - dataFeedContractName, + roles.tokenRoles[token].customFeedAdmin!, getDeploymentGenericConfig(hre, token, 'dataFeed'), ); }; @@ -672,16 +654,11 @@ export const deployMTokenDataFeedRv = async ( throw new Error('Token config is not found or customFeedRv is not set'); } - const dataFeedContractName = getTokenContractNames(token).dataFeed; - - if (!dataFeedContractName) { - throw new Error('Data feed contract name is not set'); - } - + const roles = getAllRoles(); await deployTokenDataFeed( hre, tokenAddresses.customFeedRv, - dataFeedContractName, + roles.tokenRoles[token].customFeedAdmin!, getDeploymentGenericConfig(hre, token, 'dataFeed'), ); }; @@ -701,13 +678,24 @@ export const deployMTokenCustomAggregator = async ( throw new Error('Custom aggregator contract name is not set'); } - await deployCustomAggregator(hre, customAggregatorContractName, config); + const roles = getAllRoles(); + + if (!roles.tokenRoles[token].customFeedAdmin) { + throw new Error('Custom feed admin role is not set'); + } + + await deployCustomAggregator( + hre, + customAggregatorContractName, + roles.tokenRoles[token].customFeedAdmin, + config, + ); }; const deployTokenDataFeed = async ( hre: HardhatRuntimeEnvironment, aggregator: string, - dataFeedContractName: string, + adminRole: string, networkConfig?: DeployDataFeedConfigRegular, ) => { const addresses = getCurrentAddresses(hre); @@ -716,20 +704,27 @@ const deployTokenDataFeed = async ( throw new Error('Network config is not found'); } - await deployAndVerifyProxy(hre, dataFeedContractName, [ - addresses?.accessControl, - aggregator, - networkConfig.healthyDiff ?? 2592000, - networkConfig.minAnswer ?? parseUnits('0.1', 8), - networkConfig.maxAnswer ?? parseUnits('1000', 8), - ]); + await deployAndVerifyProxy( + hre, + getCommonContractNames().dataFeed, + [ + addresses?.accessControl, + aggregator, + networkConfig.healthyDiff ?? 2592000, + networkConfig.minAnswer ?? parseUnits('0.1', 8), + networkConfig.maxAnswer ?? parseUnits('1000', 8), + ], + undefined, + { + constructorArgs: [adminRole], + }, + ); }; const deployTokenDataFeedComposite = async ( hre: HardhatRuntimeEnvironment, numeratorFeed: string, denominatorFeed: string, - dataFeedContractName: string, networkConfig?: DeployDataFeedConfigComposite, ) => { const addresses = getCurrentAddresses(hre); @@ -738,7 +733,7 @@ const deployTokenDataFeedComposite = async ( throw new Error('Network config is not found'); } - await deployAndVerifyProxy(hre, dataFeedContractName, [ + await deployAndVerifyProxy(hre, getCommonContractNames().dataFeedComposite, [ addresses?.accessControl, numeratorFeed, denominatorFeed, @@ -751,7 +746,6 @@ export const deployTokenDataFeedMultiply = async ( hre: HardhatRuntimeEnvironment, numeratorFeed: string, denominatorFeed: string, - dataFeedContractName: string, networkConfig?: DeployDataFeedConfigComposite, ) => { const addresses = getCurrentAddresses(hre); @@ -760,7 +754,7 @@ export const deployTokenDataFeedMultiply = async ( throw new Error('Network config is not found'); } - await deployAndVerifyProxy(hre, dataFeedContractName, [ + await deployAndVerifyProxy(hre, getCommonContractNames().dataFeedMultiply, [ addresses?.accessControl, numeratorFeed, denominatorFeed, @@ -772,6 +766,7 @@ export const deployTokenDataFeedMultiply = async ( const deployCustomAggregator = async ( hre: HardhatRuntimeEnvironment, customAggregatorContractName: string, + adminRole: string, networkConfig?: DeployCustomAggregatorConfig, ) => { const addresses = getCurrentAddresses(hre); @@ -798,6 +793,9 @@ const deployCustomAggregator = async ( customAggregatorContractName, params, undefined, + { + constructorArgs: [adminRole], + }, ); }; diff --git a/scripts/deploy/common/dv.ts b/scripts/deploy/common/dv.ts index f7093775..44003c58 100644 --- a/scripts/deploy/common/dv.ts +++ b/scripts/deploy/common/dv.ts @@ -10,14 +10,16 @@ import { sanctionListContracts, ustbContracts, } from '../../../config/constants/addresses'; -import { getTokenContractNames } from '../../../helpers/contracts'; +import { getCommonContractNames } from '../../../helpers/contracts'; +import { getAllRoles } from '../../../helpers/roles'; import { DepositVault, DepositVaultWithMToken, DepositVaultWithUSTB, } from '../../../typechain-types'; -export type DeployDvConfigCommon = { +export type DeployDvConfigCommonLegacy = { + version?: 'v1'; feeReceiver?: string; tokensReceiver?: `0x${string}` | RedemptionVaultType; instantDailyLimit: BigNumberish; @@ -38,6 +40,30 @@ export type DeployDvConfigCommon = { maxSupplyCap?: BigNumberish; }; +type DeployVaultConfigCommon = { + version: 'v2'; + variationTolerance: BigNumberish; + minAmount?: BigNumberish; + instantFee: BigNumberish; + enableSanctionsList?: boolean; + tokensReceiver?: `0x${string}` | RedemptionVaultType; + minInstantFee?: BigNumberish; + maxInstantFee?: BigNumberish; + maxInstantShare?: BigNumberish; + maxApproveRequestId?: BigNumberish; + sequentialRequestProcessing?: boolean; +}; + +type DeployDvConfigCommonNew = DeployVaultConfigCommon & { + minMTokenAmountForFirstDeposit?: BigNumberish; + maxSupplyCap?: BigNumberish; + maxAmountPerRequest?: BigNumberish; +}; + +export type DeployDvConfigCommon = + | DeployDvConfigCommonLegacy + | DeployDvConfigCommonNew; + export type DeployDvRegularConfig = DeployDvConfigCommon & { type?: 'REGULAR'; }; @@ -75,33 +101,25 @@ export const deployDepositVault = async ( token: MTokenName, type: 'dv' | 'dvUstb' | 'dvAave' | 'dvMorpho' | 'dvMToken', ) => { + if (token.startsWith('TAC')) { + throw new Error('TAC tokens are not supported anymore'); + } + const addresses = getCurrentAddresses(hre); const deployer = await getDeployer(hre); const tokenAddresses = addresses?.[token]; const networkConfig = getNetworkConfig(hre, token, type); - if (!tokenAddresses) { - throw new Error('Token config is not found'); + if (networkConfig.version !== 'v2') { + throw new Error('v1 configs are not supported anymore'); } - let dataFeed: string | undefined; - - if (token.startsWith('TAC')) { - const originalTokenName = token.replace('TAC', ''); - dataFeed = addresses?.[originalTokenName as MTokenName]?.dataFeed; - console.log( - `Detected TAC wrapper, will be used data feed from ${originalTokenName}: ${dataFeed}`, - ); - } else { - dataFeed = tokenAddresses?.dataFeedDv ?? tokenAddresses?.dataFeed; + if (!tokenAddresses) { + throw new Error('Token config is not found'); } - const dvContractName = getTokenContractNames(token)[type]; - - if (!dvContractName) { - throw new Error('DV contract name is not found'); - } + const dataFeed = tokenAddresses?.dataFeedDv ?? tokenAddresses?.dataFeed; const sanctionsList = networkConfig.enableSanctionsList ? sanctionListContracts[hre.network.config.chainId!] @@ -142,39 +160,55 @@ export const deployDepositVault = async ( extraParams.push(networkConfig.mTokenDepositVault); } + const ustbMTokenInitializer = + 'initialize((uint256,uint256,uint256,address,address,address,address,address,uint256,uint256,uint256,uint256,bool),(uint256,uint256,uint256),address)' as const; const params = [ - addresses?.accessControl, { + variationTolerance: networkConfig.variationTolerance, + minAmount: networkConfig.minAmount ?? 0, + instantFee: networkConfig.instantFee, + ac: addresses?.accessControl, + sanctionsList, mToken: tokenAddresses?.token, mTokenDataFeed: dataFeed, + tokensReceiver: networkConfig.tokensReceiver ?? deployer.address, + minInstantFee: networkConfig.minInstantFee ?? 0, + maxInstantFee: networkConfig.maxInstantFee ?? 100_00, + maxInstantShare: networkConfig.maxInstantShare ?? 100_00, + maxApproveRequestId: networkConfig.maxApproveRequestId ?? 100, + sequentialRequestProcessing: + networkConfig.sequentialRequestProcessing ?? false, }, { - feeReceiver: networkConfig.feeReceiver ?? deployer.address, - tokensReceiver, + minMTokenAmountForFirstDeposit: + networkConfig.minMTokenAmountForFirstDeposit ?? 0, + maxSupplyCap: networkConfig.maxSupplyCap ?? constants.MaxUint256, + maxAmountPerRequest: + networkConfig.maxAmountPerRequest ?? constants.MaxUint256, }, - { - instantDailyLimit: networkConfig.instantDailyLimit, - instantFee: networkConfig.instantFee, - }, - sanctionsList, - networkConfig.variationTolerance, - networkConfig.minAmount ?? 0, - networkConfig.minMTokenAmountForFirstDeposit ?? 0, - networkConfig.maxSupplyCap ?? constants.MaxUint256, ...extraParams, ] as | Parameters - | Parameters< - DepositVaultWithUSTB['initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,uint256,uint256,address)'] - > - | Parameters< - DepositVaultWithMToken['initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,uint256,uint256,address)'] - >; - - await deployAndVerifyProxy(hre, dvContractName, params, undefined, { - initializer: - networkConfig.type === 'USTB' || networkConfig.type === 'MTOKEN' - ? 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,uint256,uint256,address)' - : 'initialize', - }); + | Parameters + | Parameters; + + const allRoles = getAllRoles(); + const constructorParams = [ + allRoles.tokenRoles[token].depositVaultAdmin, + allRoles.tokenRoles[token].greenlisted, + ]; + + await deployAndVerifyProxy( + hre, + getCommonContractNames()[type], + params, + undefined, + { + constructorArgs: constructorParams, + initializer: + networkConfig.type === 'USTB' || networkConfig.type === 'MTOKEN' + ? ustbMTokenInitializer + : 'initialize', + }, + ); }; diff --git a/scripts/deploy/common/roles.ts b/scripts/deploy/common/roles.ts index 388c88d6..2180c515 100644 --- a/scripts/deploy/common/roles.ts +++ b/scripts/deploy/common/roles.ts @@ -63,7 +63,7 @@ export const grantAllProductRoles = async ( allRoles.common.greenlistedOperator, tokenRoles.burner, tokenRoles.minter, - tokenRoles.pauser, + tokenRoles.tokenManager, ]; const vaultManagerRoles = [ @@ -132,8 +132,11 @@ export const grantAllProductRoles = async ( await sendAndWaitForCustomTxSign( hre, await accessControl.populateTransaction.grantRoleMult( - rolesToGrant, - addressesToGrant, + rolesToGrant.map((role, index) => ({ + role, + account: addressesToGrant[index], + delay: 0, // TODO: add default delay? + })), ), { action: 'update-ac', @@ -156,8 +159,7 @@ export const revokeDefaultRolesFromDeployer = async ( await sendAndWaitForCustomTxSign( hre, await accessControl.populateTransaction.revokeRoleMult( - roles, - roles.map(() => deployer.address), + roles.map((role) => ({ role, account: deployer.address })), ), { action: 'deployer', @@ -188,7 +190,7 @@ export const grantDefaultAdminRoleToAcAdmin = async ( await sendAndWaitForCustomTxSign( hre, - await accessControl.populateTransaction.grantRole( + await accessControl.populateTransaction['grantRole(bytes32,address)']( allRoles.common.defaultAdmin, networkConfig?.acAdminAddress ?? acAdminAddress, ), diff --git a/scripts/deploy/common/rv.ts b/scripts/deploy/common/rv.ts index 2f8de38a..3656f762 100644 --- a/scripts/deploy/common/rv.ts +++ b/scripts/deploy/common/rv.ts @@ -9,16 +9,17 @@ import { getCurrentAddresses, RedemptionVaultType, sanctionListContracts, + TokenAddresses, } from '../../../config/constants/addresses'; -import { getTokenContractNames } from '../../../helpers/contracts'; +import { getCommonContractNames } from '../../../helpers/contracts'; +import { getAllRoles } from '../../../helpers/roles'; import { - MBasisRedemptionVaultWithSwapper, RedemptionVault, RedemptionVaultWithMToken, - RedemptionVaultWIthBUIDL, } from '../../../typechain-types'; -export type DeployRvConfigCommon = { +export type DeployRvConfigCommonLegacy = { + version?: 'v1'; feeReceiver?: string; tokensReceiver?: string; instantDailyLimit: BigNumberish; @@ -44,16 +45,19 @@ export type DeployRvConfigCommon = { minFiatRedeemAmount?: BigNumberish; }; -export type DeployRvRegularConfig = { - type: 'REGULAR'; -} & DeployRvConfigCommon; - -export type DeployRvBuidlConfig = { - type: 'BUIDL'; - buidlRedemption: string; - minBuidlBalance: BigNumberish; - minBuidlToRedeem: BigNumberish; -} & DeployRvConfigCommon; +type DeployVaultConfigCommon = { + version: 'v2'; + variationTolerance: BigNumberish; + minAmount?: BigNumberish; + instantFee: BigNumberish; + enableSanctionsList?: boolean; + tokensReceiver?: string; + minInstantFee?: BigNumberish; + maxInstantFee?: BigNumberish; + maxInstantShare?: BigNumberish; + maxApproveRequestId?: BigNumberish; + sequentialRequestProcessing?: boolean; +}; type SwapperVault = | { @@ -62,6 +66,25 @@ type SwapperVault = } | 'dummy'; +type DeployRvConfigCommonNew = DeployVaultConfigCommon & { + requestRedeemer?: string; + loanConfig?: { + loanLp?: string; + loanRepaymentAddress?: string; + loanApr: BigNumberish; + loanSwapperVault: Exclude; + }; +}; + +export type DeployRvConfigCommon = + | DeployRvConfigCommonLegacy + | DeployRvConfigCommonNew; + +export type DeployRvRegularConfig = { + type: 'REGULAR'; +} & DeployRvConfigCommon; + +// TODO: remove export type DeployRvSwapperConfig = { type: 'SWAPPER'; swapperVault: SwapperVault; @@ -83,7 +106,6 @@ export type DeployRvMTokenConfig = { export type DeployRvConfig = | DeployRvRegularConfig - | DeployRvBuidlConfig | DeployRvSwapperConfig | DeployRvAaveConfig | DeployRvMorphoConfig @@ -91,11 +113,48 @@ export type DeployRvConfig = const DUMMY_ADDRESS = '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'; +const resolveLoanAddresses = async ( + hre: HardhatRuntimeEnvironment, + networkConfig: DeployRvConfigCommonNew, + addresses: Record, +) => { + if (!networkConfig.loanConfig) { + return { + loanLp: constants.AddressZero, + loanRepaymentAddress: constants.AddressZero, + loanSwapperVault: constants.AddressZero, + loanApr: constants.AddressZero, + }; + } + const swapperVault = networkConfig.loanConfig.loanSwapperVault; + + const swapperVaultAddress = + addresses[swapperVault.mToken]?.[swapperVault.redemptionVaultType]; + + if (!swapperVaultAddress) { + throw new Error('Swapper vault address is not found'); + } + + const deployer = await getDeployer(hre); + + return { + loanLp: networkConfig.loanConfig.loanLp ?? deployer.address, + loanRepaymentAddress: + networkConfig.loanConfig.loanRepaymentAddress ?? deployer.address, + loanSwapperVault: swapperVaultAddress, + loanApr: networkConfig.loanConfig.loanApr, + }; +}; + export const deployRedemptionVault = async ( hre: HardhatRuntimeEnvironment, token: MTokenName, - type: 'rv' | 'rvBuidl' | 'rvSwapper' | 'rvAave' | 'rvMorpho' | 'rvMToken', + type: 'rv' | 'rvSwapper' | 'rvAave' | 'rvMorpho' | 'rvMToken', ) => { + if (token.startsWith('TAC')) { + throw new Error('TAC tokens are not supported anymore'); + } + const addresses = getCurrentAddresses(hre); const deployer = await getDeployer(hre); const tokenAddresses = addresses?.[token]; @@ -106,60 +165,23 @@ export const deployRedemptionVault = async ( throw new Error('Token config is not found'); } - const contractName = getTokenContractNames(token)[type]; + if (networkConfig.version !== 'v2') { + throw new Error('v1 configs are not supported anymore'); + } - if (!contractName) { - throw new Error('Unsupported token/type combination'); + if (networkConfig.type === 'SWAPPER') { + throw new Error('Swapper configs are not supported anymore'); } + const allRoles = getAllRoles(); + const extraParams: unknown[] = []; if (networkConfig.type === 'MTOKEN') { extraParams.push(networkConfig.redemptionVault); - } else if (networkConfig.type === 'BUIDL') { - extraParams.push(networkConfig.buidlRedemption); - extraParams.push(networkConfig.minBuidlToRedeem); - extraParams.push(networkConfig.minBuidlBalance); - } else if (networkConfig.type === 'SWAPPER') { - const swapperVault = networkConfig.swapperVault; - - let swapperVaultAddress: string | undefined; - - if (swapperVault === 'dummy') { - swapperVaultAddress = DUMMY_ADDRESS; - } else { - swapperVaultAddress = - addresses[swapperVault.mToken]?.[swapperVault.redemptionVaultType]; - } - - if (!swapperVaultAddress) { - throw new Error('Swapper vault address is not found'); - } - - if (swapperVaultAddress === DUMMY_ADDRESS) { - console.log('Using dummy swapper vault address'); - } - - const liquidityProvider = - networkConfig.liquidityProvider === 'dummy' - ? DUMMY_ADDRESS - : networkConfig.liquidityProvider ?? deployer.address; - - extraParams.push(swapperVaultAddress); - extraParams.push(liquidityProvider); } - let dataFeed: string | undefined; - - if (token.startsWith('TAC')) { - const originalTokenName = token.replace('TAC', ''); - dataFeed = addresses?.[originalTokenName as MTokenName]?.dataFeed; - console.log( - `Detected TAC wrapper, will be used data feed from ${originalTokenName}: ${dataFeed}`, - ); - } else { - dataFeed = tokenAddresses?.dataFeedRv ?? tokenAddresses?.dataFeed; - } + const dataFeed = tokenAddresses?.dataFeedRv ?? tokenAddresses?.dataFeed; const sanctionsList = networkConfig.enableSanctionsList ? sanctionListContracts[hre.network.config.chainId!] @@ -169,52 +191,55 @@ export const deployRedemptionVault = async ( throw new Error('Sanctions list address is not found'); } + // FIXME: fix according to new initialize params const params = [ - addresses?.accessControl, { + variationTolerance: networkConfig.variationTolerance, + minAmount: networkConfig.minAmount ?? parseUnits('0', 18), + instantFee: networkConfig.instantFee, + ac: addresses?.accessControl, + sanctionsList, mToken: tokenAddresses?.token, mTokenDataFeed: dataFeed, - }, - { - feeReceiver: networkConfig.feeReceiver ?? deployer.address, tokensReceiver: networkConfig.tokensReceiver ?? deployer.address, + minInstantFee: networkConfig.minInstantFee ?? 0, + maxInstantFee: networkConfig.maxInstantFee ?? 100_00, + maxInstantShare: networkConfig.maxInstantShare ?? 100_00, + maxApproveRequestId: networkConfig.maxApproveRequestId ?? 100, + sequentialRequestProcessing: + networkConfig.sequentialRequestProcessing ?? false, }, { - instantDailyLimit: networkConfig.instantDailyLimit, - instantFee: networkConfig.instantFee, - }, - sanctionsList, - networkConfig.variationTolerance, - networkConfig.minAmount ?? parseUnits('0', 18), - { - fiatAdditionalFee: - networkConfig.fiatAdditionalFee ?? parseUnits('0.1', 2), - fiatFlatFee: networkConfig.fiatFlatFee ?? parseUnits('30', 18), - minFiatRedeemAmount: - networkConfig.minFiatRedeemAmount ?? parseUnits('1000', 18), + requestRedeemer: networkConfig.requestRedeemer ?? deployer.address, + ...(await resolveLoanAddresses( + hre, + networkConfig, + addresses as Record, + )), }, - networkConfig.requestRedeemer ?? deployer.address, ...extraParams, ] as | Parameters | Parameters< - RedemptionVaultWIthBUIDL['initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address,uint256,uint256)'] - > - | Parameters< - MBasisRedemptionVaultWithSwapper['initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address,address)'] - > - | Parameters< - RedemptionVaultWithMToken['initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address)'] + RedemptionVaultWithMToken['initialize((uint256,uint256,uint256,address,address,address,address,address,uint256,uint256,uint256,uint256,bool),(address,address,address,address,uint256),address)'] >; - await deployAndVerifyProxy(hre, contractName, params, undefined, { - initializer: - networkConfig.type === 'SWAPPER' - ? 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address,address)' - : networkConfig.type === 'BUIDL' - ? 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address,uint256,uint256)' - : networkConfig.type === 'MTOKEN' - ? 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address)' - : 'initialize', - }); + const constructorParams = [ + allRoles.tokenRoles[token].redemptionVaultAdmin, + allRoles.tokenRoles[token].greenlisted, + ]; + + await deployAndVerifyProxy( + hre, + getCommonContractNames()[type], + params, + undefined, + { + constructorArgs: constructorParams, + initializer: + networkConfig.type === 'MTOKEN' + ? 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address)' + : 'initialize', + }, + ); }; diff --git a/scripts/deploy/common/token.ts b/scripts/deploy/common/token.ts index fd869816..bef633a1 100644 --- a/scripts/deploy/common/token.ts +++ b/scripts/deploy/common/token.ts @@ -4,7 +4,8 @@ import { deployAndVerifyProxy } from './utils'; import { MTokenName } from '../../../config'; import { getCurrentAddresses } from '../../../config/constants/addresses'; -import { getTokenContractNames } from '../../../helpers/contracts'; +import { getCommonContractNames } from '../../../helpers/contracts'; +import { getAllRoles } from '../../../helpers/roles'; export const deployMToken = async ( hre: HardhatRuntimeEnvironment, @@ -15,7 +16,19 @@ export const deployMToken = async ( if (!addresses?.accessControl) throw new Error('Access control address is not set'); - const tokenContractName = getTokenContractNames(token).token; + const allRoles = getAllRoles(); - await deployAndVerifyProxy(hre, tokenContractName, [addresses.accessControl]); + await deployAndVerifyProxy( + hre, + getCommonContractNames().mToken, + [addresses.accessControl], + undefined, + { + constructorArgs: [ + allRoles.tokenRoles[token].tokenManager, + allRoles.tokenRoles[token].minter, + allRoles.tokenRoles[token].burner, + ], + }, + ); }; diff --git a/scripts/deploy/common/types.ts b/scripts/deploy/common/types.ts index 2a1af4f8..4d0c7b9c 100644 --- a/scripts/deploy/common/types.ts +++ b/scripts/deploy/common/types.ts @@ -27,7 +27,6 @@ import { } from './roles'; import { DeployRvAaveConfig, - DeployRvBuidlConfig, DeployRvMorphoConfig, DeployRvMTokenConfig, DeployRvRegularConfig, @@ -40,6 +39,7 @@ import { PartialConfigPerNetwork, PaymentTokenName } from '../../../config'; import { VaultType } from '../../../config/constants/addresses'; import { RateLimiter } from '../../../typechain-types'; +// TODO: fix selectors export const VAULT_FUNCTION_SELECTORS = { // Deposit vault functions depositInstant: toFunctionSelector( @@ -117,7 +117,6 @@ export type DeploymentConfig = { dvMorpho?: DeployDvMorphoConfig; dvMToken?: DeployDvMTokenConfig; rv?: DeployRvRegularConfig; - rvBuidl?: DeployRvBuidlConfig; rvSwapper?: DeployRvSwapperConfig; rvAave?: DeployRvAaveConfig; rvMorpho?: DeployRvMorphoConfig; diff --git a/scripts/deploy/common/utils.ts b/scripts/deploy/common/utils.ts index 7040b294..9ed00951 100644 --- a/scripts/deploy/common/utils.ts +++ b/scripts/deploy/common/utils.ts @@ -175,6 +175,7 @@ export const deployAndVerifyProxy = async ( await tryEtherscanVerifyImplementation( hre, deployment.address, + contractName, ...(opts?.constructorArgs ?? []), ); diff --git a/scripts/deploy/common/vault-resolver.ts b/scripts/deploy/common/vault-resolver.ts index d8debb03..01e9a7dd 100644 --- a/scripts/deploy/common/vault-resolver.ts +++ b/scripts/deploy/common/vault-resolver.ts @@ -19,13 +19,11 @@ export const routingRedemptionVaultPriority: RedemptionVaultType[] = [ 'redemptionVaultAave', 'redemptionVaultMorpho', 'redemptionVault', - 'redemptionVaultBuidl', ]; export const roleGrantRedemptionVaultPriority: RedemptionVaultType[] = [ 'redemptionVaultMToken', 'redemptionVaultSwapper', - 'redemptionVaultBuidl', 'redemptionVaultUstb', 'redemptionVaultAave', 'redemptionVaultMorpho', diff --git a/scripts/deploy/configs/hypeBTC.ts b/scripts/deploy/configs/hypeBTC.ts index dcafe54b..3b56806f 100644 --- a/scripts/deploy/configs/hypeBTC.ts +++ b/scripts/deploy/configs/hypeBTC.ts @@ -44,7 +44,7 @@ export const hypeBTCDeploymentConfig: DeploymentConfig = { liquidityProvider: undefined, swapperVault: { mToken: 'mTBILL', - redemptionVaultType: 'redemptionVaultBuidl', + redemptionVaultType: 'redemptionVaultUstb', }, }, }, diff --git a/scripts/deploy/configs/mAPOLLO.ts b/scripts/deploy/configs/mAPOLLO.ts index c25223f9..4b9c9a38 100644 --- a/scripts/deploy/configs/mAPOLLO.ts +++ b/scripts/deploy/configs/mAPOLLO.ts @@ -44,7 +44,7 @@ export const mAPOLLODeploymentConfig: DeploymentConfig = { liquidityProvider: '0x0461bD693caE49bE9d030E5c212e080F9c78B846', swapperVault: { mToken: 'mTBILL', - redemptionVaultType: 'redemptionVaultBuidl', + redemptionVaultType: 'redemptionVaultUstb', }, enableSanctionsList: true, }, diff --git a/scripts/deploy/configs/mBASIS.ts b/scripts/deploy/configs/mBASIS.ts index 6d291a82..ae348b37 100644 --- a/scripts/deploy/configs/mBASIS.ts +++ b/scripts/deploy/configs/mBASIS.ts @@ -44,7 +44,7 @@ export const mBASISDeploymentConfig: DeploymentConfig = { liquidityProvider: undefined, swapperVault: { mToken: 'mTBILL', - redemptionVaultType: 'redemptionVaultBuidl', + redemptionVaultType: 'redemptionVaultUstb', }, }, }, @@ -75,7 +75,7 @@ export const mBASISDeploymentConfig: DeploymentConfig = { liquidityProvider: '0x7388e98baCfFF1B3618d7d5bEbeDe483C9526FEd', swapperVault: { mToken: 'mTBILL', - redemptionVaultType: 'redemptionVaultBuidl', + redemptionVaultType: 'redemptionVaultUstb', }, }, }, diff --git a/scripts/deploy/configs/mFARM.ts b/scripts/deploy/configs/mFARM.ts index 69ad59bc..9a8f0caa 100644 --- a/scripts/deploy/configs/mFARM.ts +++ b/scripts/deploy/configs/mFARM.ts @@ -44,7 +44,7 @@ export const mFARMDeploymentConfig: DeploymentConfig = { liquidityProvider: undefined, swapperVault: { mToken: 'mTBILL', - redemptionVaultType: 'redemptionVaultBuidl', + redemptionVaultType: 'redemptionVaultUstb', }, }, postDeploy: { diff --git a/scripts/deploy/configs/mFONE.ts b/scripts/deploy/configs/mFONE.ts index 23695d4a..0878756d 100644 --- a/scripts/deploy/configs/mFONE.ts +++ b/scripts/deploy/configs/mFONE.ts @@ -48,7 +48,7 @@ export const mFONEDeploymentConfig: DeploymentConfig = { liquidityProvider: undefined, swapperVault: { mToken: 'mTBILL', - redemptionVaultType: 'redemptionVaultBuidl', + redemptionVaultType: 'redemptionVaultUstb', }, }, postDeploy: { @@ -81,7 +81,7 @@ export const mFONEDeploymentConfig: DeploymentConfig = { liquidityProvider: '0x4dc293e0d6BEfe6FCF9d1FFDEaA5266BD15C3071', swapperVault: { mToken: 'mTBILL', - redemptionVaultType: 'redemptionVaultBuidl', + redemptionVaultType: 'redemptionVaultUstb', }, enableSanctionsList: true, }, diff --git a/scripts/deploy/configs/mHYPER.ts b/scripts/deploy/configs/mHYPER.ts index b47d8f82..feaef211 100644 --- a/scripts/deploy/configs/mHYPER.ts +++ b/scripts/deploy/configs/mHYPER.ts @@ -46,7 +46,7 @@ export const mHYPERDeploymentConfig: DeploymentConfig = { liquidityProvider: '0x0461bD693caE49bE9d030E5c212e080F9c78B846', swapperVault: { mToken: 'mTBILL', - redemptionVaultType: 'redemptionVaultBuidl', + redemptionVaultType: 'redemptionVaultUstb', }, enableSanctionsList: true, }, diff --git a/scripts/deploy/configs/mSL.ts b/scripts/deploy/configs/mSL.ts index 5686af16..23558326 100644 --- a/scripts/deploy/configs/mSL.ts +++ b/scripts/deploy/configs/mSL.ts @@ -1,4 +1,3 @@ -import { constants } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; import { chainIds } from '../../../config'; @@ -21,30 +20,45 @@ export const mSLDeploymentConfig: DeploymentConfig = { networkConfigs: { [chainIds.sepolia]: { dv: { - feeReceiver: undefined, - tokensReceiver: undefined, - instantDailyLimit: constants.MaxUint256, + version: 'v2', instantFee: parseUnits('1', 2), - minMTokenAmountForFirstDeposit: parseUnits('100'), - minAmount: parseUnits('0.01'), variationTolerance: parseUnits('0.1', 2), }, - rvSwapper: { - type: 'SWAPPER', - feeReceiver: undefined, - tokensReceiver: undefined, - instantDailyLimit: constants.MaxUint256, + rv: { + type: 'REGULAR', + version: 'v2', instantFee: parseUnits('1', 2), - minAmount: parseUnits('0.01'), variationTolerance: parseUnits('0.1', 2), - fiatAdditionalFee: parseUnits('0.1', 2), - fiatFlatFee: parseUnits('0.1', 18), - minFiatRedeemAmount: parseUnits('1', 18), - requestRedeemer: undefined, - liquidityProvider: undefined, - swapperVault: { - mToken: 'mTBILL', - redemptionVaultType: 'redemptionVaultBuidl', + }, + postDeploy: { + grantRoles: { + tokenManagerAddress: '0xa0819ae43115420beb161193b8D8Ba64C9f9faCC', + oracleManagerAddress: '0xa0819ae43115420beb161193b8D8Ba64C9f9faCC', + vaultsManagerAddress: '0xa0819ae43115420beb161193b8D8Ba64C9f9faCC', + }, + addPaymentTokens: { + vaults: [ + { + type: 'depositVault', + paymentTokens: [ + { + token: 'usdc', + }, + ], + }, + { + type: 'redemptionVault', + paymentTokens: [ + { + token: 'usdc', + }, + ], + }, + ], + }, + setRoundData: { + type: 'REGULAR', + data: parseUnits('1', 8), }, }, }, @@ -74,7 +88,7 @@ export const mSLDeploymentConfig: DeploymentConfig = { liquidityProvider: '0x0461bD693caE49bE9d030E5c212e080F9c78B846', swapperVault: { mToken: 'mTBILL', - redemptionVaultType: 'redemptionVaultBuidl', + redemptionVaultType: 'redemptionVaultUstb', }, enableSanctionsList: true, }, diff --git a/scripts/deploy/configs/mTBILL.ts b/scripts/deploy/configs/mTBILL.ts index 5f2464f0..c9cf7e20 100644 --- a/scripts/deploy/configs/mTBILL.ts +++ b/scripts/deploy/configs/mTBILL.ts @@ -22,45 +22,64 @@ export const mTBILLDeploymentConfig: DeploymentConfig = { networkConfigs: { [chainIds.sepolia]: { dv: { - feeReceiver: undefined, - tokensReceiver: undefined, - instantDailyLimit: constants.MaxUint256, + version: 'v2', instantFee: parseUnits('1', 2), - minMTokenAmountForFirstDeposit: parseUnits('100'), - minAmount: parseUnits('0.01'), variationTolerance: parseUnits('0.1', 2), }, rv: { + version: 'v2', type: 'REGULAR', - feeReceiver: undefined, - tokensReceiver: undefined, - instantDailyLimit: constants.MaxUint256, - instantFee: parseUnits('1', 2), - minAmount: parseUnits('0.01'), - variationTolerance: parseUnits('0.1', 2), - fiatAdditionalFee: parseUnits('0.1', 2), - fiatFlatFee: parseUnits('0.1', 18), - minFiatRedeemAmount: parseUnits('1', 18), - requestRedeemer: undefined, - }, - rvBuidl: { - type: 'BUIDL', - feeReceiver: undefined, - tokensReceiver: undefined, - instantDailyLimit: constants.MaxUint256, instantFee: parseUnits('1', 2), - minAmount: parseUnits('0.01'), variationTolerance: parseUnits('0.1', 2), - fiatAdditionalFee: parseUnits('0.1', 2), - fiatFlatFee: parseUnits('0.1', 18), - minFiatRedeemAmount: parseUnits('1', 18), - requestRedeemer: undefined, - buidlRedemption: '0x4cb1479705EA6F0dD63415111aF56eaCfBa019bb', // mocked BUIDL redemption - minBuidlBalance: parseUnits('250000', 18), - minBuidlToRedeem: parseUnits('250000', 18), + loanConfig: { + loanApr: parseUnits('0.1', 2), + loanSwapperVault: { + mToken: 'mSL', + redemptionVaultType: 'redemptionVault', + }, + }, }, postDeploy: { - grantRoles: {}, + setRoundData: { + type: 'GROWTH', + data: parseUnits('1', 8), + apr: parseUnits('0.1', 8), + }, + grantRoles: { + tokenManagerAddress: '0xa0819ae43115420beb161193b8D8Ba64C9f9faCC', + oracleManagerAddress: '0xa0819ae43115420beb161193b8D8Ba64C9f9faCC', + vaultsManagerAddress: '0xa0819ae43115420beb161193b8D8Ba64C9f9faCC', + }, + addPaymentTokens: { + vaults: [ + { + type: 'depositVault', + paymentTokens: [ + { + token: 'usdc', + }, + ], + }, + { + type: 'redemptionVault', + paymentTokens: [ + { + token: 'usdc', + }, + ], + }, + ], + }, + addFeeWaived: [ + { + fromVault: { + mToken: 'mSL', + type: 'redemptionVault', + }, + toWaive: [{ mToken: 'mTBILL', type: 'redemptionVault' }], + value: true, + }, + ], axelarIts: { operator: '0xa0819ae43115420beb161193b8D8Ba64C9f9faCC', }, @@ -162,26 +181,7 @@ export const mTBILLDeploymentConfig: DeploymentConfig = { requestRedeemer: '0x1Bd4d8D25Ec7EBA10e94BE71Fd9c6BF672e31E06', enableSanctionsList: true, }, - rvBuidl: { - type: 'BUIDL', - feeReceiver: '0x875c06A295C41c27840b9C9dfDA7f3d819d8bC6A', - tokensReceiver: '0x1Bd4d8D25Ec7EBA10e94BE71Fd9c6BF672e31E06', - instantDailyLimit: parseUnits('1000'), - instantFee: parseUnits('0.07', 2), - minAmount: parseUnits('0.1'), - variationTolerance: parseUnits('0.1', 2), - fiatAdditionalFee: parseUnits('0.1', 2), - fiatFlatFee: parseUnits('30', 18), - minFiatRedeemAmount: parseUnits('1000', 18), - requestRedeemer: '0x1Bd4d8D25Ec7EBA10e94BE71Fd9c6BF672e31E06', - enableSanctionsList: true, - buidlRedemption: '0x31D3F59Ad4aAC0eeE2247c65EBE8Bf6E9E470a53', - minBuidlBalance: parseUnits('1', 6), - minBuidlToRedeem: parseUnits('1', 6), - }, - postDeploy: { - grantRoles: {}, - }, + postDeploy: {}, }, [chainIds.base]: { dv: { diff --git a/scripts/deploy/deploy_PauseManager.ts b/scripts/deploy/deploy_PauseManager.ts new file mode 100644 index 00000000..8570131b --- /dev/null +++ b/scripts/deploy/deploy_PauseManager.ts @@ -0,0 +1,24 @@ +import { BigNumber } from 'ethers'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +import { DeployFunction } from './common/types'; +import { deployAndVerifyProxy } from './common/utils'; + +import { getCurrentAddresses } from '../../config/constants/addresses'; +import { getCommonContractNames } from '../../helpers/contracts'; + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const addresses = getCurrentAddresses(hre); + + if (!addresses?.accessControl) { + throw new Error('Access control address is not set'); + } + + await deployAndVerifyProxy(hre, getCommonContractNames().pauseManager, [ + addresses.accessControl, + BigNumber.from('0xFFFFFFFF'), + 3600, + ]); +}; + +export default func; diff --git a/scripts/deploy/deploy_RVBuidl.ts b/scripts/deploy/deploy_RVBuidl.ts deleted file mode 100644 index 2fddc01d..00000000 --- a/scripts/deploy/deploy_RVBuidl.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types'; - -import { deployRedemptionVault } from './common'; -import { DeployFunction } from './common/types'; - -import { getMTokenOrThrow } from '../../helpers/utils'; - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const mToken = getMTokenOrThrow(hre); - await deployRedemptionVault(hre, mToken, 'rvBuidl'); -}; - -export default func; diff --git a/scripts/deploy/deploy_RVSwapper.ts b/scripts/deploy/deploy_RVSwapper.ts deleted file mode 100644 index 60115958..00000000 --- a/scripts/deploy/deploy_RVSwapper.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types'; - -import { deployRedemptionVault } from './common'; -import { DeployFunction } from './common/types'; - -import { getMTokenOrThrow } from '../../helpers/utils'; - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const mToken = getMTokenOrThrow(hre); - await deployRedemptionVault(hre, mToken, 'rvSwapper'); -}; - -export default func; diff --git a/scripts/deploy/deploy_TimelockController.ts b/scripts/deploy/deploy_TimelockController.ts new file mode 100644 index 00000000..55b0bbdd --- /dev/null +++ b/scripts/deploy/deploy_TimelockController.ts @@ -0,0 +1,21 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +import { DeployFunction } from './common/types'; +import { deployAndVerifyProxy } from './common/utils'; + +import { getCurrentAddresses } from '../../config/constants/addresses'; +import { getCommonContractNames } from '../../helpers/contracts'; + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const addresses = getCurrentAddresses(hre); + + if (!addresses?.timelockManager) { + throw new Error('Timelock manager address is not set'); + } + + await deployAndVerifyProxy(hre, getCommonContractNames().timelockController, [ + addresses.timelockManager, + ]); +}; + +export default func; diff --git a/scripts/deploy/deploy_TimelockManager.ts b/scripts/deploy/deploy_TimelockManager.ts new file mode 100644 index 00000000..c2c42851 --- /dev/null +++ b/scripts/deploy/deploy_TimelockManager.ts @@ -0,0 +1,36 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +import { DeployFunction } from './common/types'; +import { deployAndVerifyProxy } from './common/utils'; + +import { getCurrentAddresses } from '../../config/constants/addresses'; +import { getCommonContractNames } from '../../helpers/contracts'; + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const [ + councilMember1, + councilMember2, + councilMember3, + councilMember4, + councilMember5, + ] = await hre.ethers.getSigners(); + const addresses = getCurrentAddresses(hre); + + if (!addresses?.accessControl) { + throw new Error('Access control address is not set'); + } + + await deployAndVerifyProxy(hre, getCommonContractNames().timelockManager, [ + addresses.accessControl, + 100, + [ + councilMember1.address, + councilMember2.address, + councilMember3.address, + councilMember4.address, + councilMember5.address, + ], + ]); +}; + +export default func; diff --git a/scripts/deploy/misc/acre/deploy_AcreAdapter.ts b/scripts/deploy/misc/acre/deploy_AcreAdapter.ts index ccaba3aa..d07c6968 100644 --- a/scripts/deploy/misc/acre/deploy_AcreAdapter.ts +++ b/scripts/deploy/misc/acre/deploy_AcreAdapter.ts @@ -67,9 +67,6 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { { value: 'redemptionVaultSwapper', }, - { - value: 'redemptionVaultBuidl', - }, { value: 'redemptionVaultUstb', }, diff --git a/scripts/deploy/misc/mocs/deploy_BuidlRedemptionMock.ts b/scripts/deploy/misc/mocs/deploy_BuidlRedemptionMock.ts deleted file mode 100644 index bc60e22d..00000000 --- a/scripts/deploy/misc/mocs/deploy_BuidlRedemptionMock.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types'; - -import { getCurrentAddresses } from '../../../../config/constants/addresses'; -import { etherscanVerify } from '../../../../helpers/utils'; -import { RedemptionTest__factory } from '../../../../typechain-types'; -import { DeployFunction } from '../../common/types'; -import { getDeployer } from '../../common/utils'; - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const deployer = await getDeployer(hre); - - const addresses = getCurrentAddresses(hre); - - console.log('Deploying BuidlRedemptionMock...'); - - const deployment = await new RedemptionTest__factory(deployer).deploy( - '0xE6e05cf306d41585BEE8Ae48F9f2DD7E0955e6D3', // test BUIDL token on sepolia - addresses!.paymentTokens!.usdc!.token!, - ); - - console.log('Deployed BuidlRedemptionMock :', deployment.address); - - if (deployment.deployTransaction) { - console.log('Waiting 5 blocks...'); - await deployment.deployTransaction.wait(5); - console.log('Waited.'); - } - await etherscanVerify(hre, deployment.address); -}; - -export default func; diff --git a/scripts/deploy/post-deploy/add_FeeWaived.ts b/scripts/deploy/post-deploy/add_FeeWaived.ts index 8d54e4c4..97021ba4 100644 --- a/scripts/deploy/post-deploy/add_FeeWaived.ts +++ b/scripts/deploy/post-deploy/add_FeeWaived.ts @@ -1,12 +1,12 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { getMTokenOrThrow } from '../../../helpers/utils'; -import { addFeeWaived } from '../common/common-vault'; +import { setFeeWaivedAccounts } from '../common/common-vault'; import { DeployFunction } from '../common/types'; const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const mToken = getMTokenOrThrow(hre); - await addFeeWaived(hre, mToken); + await setFeeWaivedAccounts(hre, mToken); }; export default func; diff --git a/scripts/deploy/post-deploy/set_SanctionsList.ts b/scripts/deploy/post-deploy/set_SanctionsList.ts index d43a1a20..45a4038a 100644 --- a/scripts/deploy/post-deploy/set_SanctionsList.ts +++ b/scripts/deploy/post-deploy/set_SanctionsList.ts @@ -44,10 +44,6 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { type: 'redemptionVault', address: tokenAddresses.redemptionVault, }, - { - type: 'redemptionVaultBuidl', - address: tokenAddresses.redemptionVaultBuidl, - }, { type: 'redemptionVaultSwapper', address: tokenAddresses.redemptionVaultSwapper, diff --git a/scripts/deploy/post-deploy/wire_AccessControl.ts b/scripts/deploy/post-deploy/wire_AccessControl.ts new file mode 100644 index 00000000..4de3ca89 --- /dev/null +++ b/scripts/deploy/post-deploy/wire_AccessControl.ts @@ -0,0 +1,79 @@ +import { ethers } from 'ethers'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +import { getCurrentAddresses } from '../../../config/constants/addresses'; +import { + MidasAccessControl__factory, + MidasTimelockManager__factory, +} from '../../../typechain-types'; +import { DeployFunction } from '../common/types'; +import { sendAndWaitForCustomTxSign } from '../common/utils'; + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const addresses = getCurrentAddresses(hre); + + if (!addresses?.accessControl) { + throw new Error('Access control address is not set'); + } + + if (!addresses?.timelockManager) { + throw new Error('Timelock manager address is not set'); + } + + if (!addresses?.timelockController) { + throw new Error('Timelock controller address is not set'); + } + + if (!addresses?.pauseManager) { + throw new Error('Pause manager address is not set'); + } + + const accessControl = MidasAccessControl__factory.connect( + addresses.accessControl, + hre.ethers.provider, + ); + + const timelockManager = MidasTimelockManager__factory.connect( + addresses.timelockManager, + hre.ethers.provider, + ); + + const timelockManagerWired = + (await timelockManager.timelock()) !== ethers.constants.AddressZero; + + const accessControlWired = + (await accessControl.timelockManager()) !== ethers.constants.AddressZero; + + if (!timelockManagerWired) { + const tx = await timelockManager.populateTransaction.initializeTimelock( + addresses.timelockController, + ); + const txRes = await sendAndWaitForCustomTxSign(hre, tx, { + comment: `Initialize timelock controller in timelock manager`, + action: 'update-ac', + }); + + console.log(`Initialize timelock controller in timelock manager`, txRes); + } else { + console.log('Timelock controller is already wired'); + } + + if (!accessControlWired) { + const tx = await accessControl.populateTransaction.initializeRelationships( + addresses.timelockManager, + addresses.pauseManager, + ); + const txRes = await sendAndWaitForCustomTxSign(hre, tx, { + comment: `Initialize relationships in access control`, + action: 'update-ac', + }); + + console.log(`Initialize relationships in access control`, txRes); + } else { + console.log('Relationships in access control are already wired'); + } + + console.log('Wire transactions sent'); +}; + +export default func; diff --git a/scripts/generate-errors-readme.ts b/scripts/generate-errors-readme.ts new file mode 100644 index 00000000..dab35990 --- /dev/null +++ b/scripts/generate-errors-readme.ts @@ -0,0 +1,107 @@ +// Generates a registry of every custom error across our compiled contracts. +// +// yarn compile # make sure artifacts are fresh +// yarn docs:errors # writes ERRORS.md +// yarn docs:errors 0x8d666f60 # look up a single selector +// +// Reads Hardhat artifacts under artifacts/contracts (our own contracts only). +// Inherited errors from dependencies (OpenZeppelin, Chainlink CCIP, ...) are +// still captured because Hardhat flattens them into each contract's ABI. +import { ethers } from 'ethers'; + +import { readdirSync, readFileSync, writeFileSync, statSync } from 'fs'; +import { join } from 'path'; + +const repoRoot = join(__dirname, '..'); +const artifactsDir = join(repoRoot, 'artifacts', 'contracts'); +const outFile = join(repoRoot, 'ERRORS.md'); + +// Directory names to skip anywhere in the tree. +const skipDirs = new Set(['build-info', 'mocks', 'testers']); + +const lookup = process.argv[2]?.toLowerCase(); + +type ErrorEntry = { signature: string; sources: Set }; + +// Recursively collect artifact JSON files (skip debug + excluded dirs). +const walk = (dir: string): string[] => { + const out: string[] = []; + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + if (statSync(full).isDirectory()) { + if (skipDirs.has(entry)) continue; + out.push(...walk(full)); + } else if (entry.endsWith('.json') && !entry.endsWith('.dbg.json')) { + out.push(full); + } + } + return out; +}; + +// selector -> { signature, sources } +const errors = new Map(); + +for (const file of walk(artifactsDir)) { + let artifact: { contractName?: string; abi?: unknown[] }; + try { + artifact = JSON.parse(readFileSync(file, 'utf8')); + } catch { + continue; + } + if (!Array.isArray(artifact.abi)) continue; + const contractName = artifact.contractName || file; + + let iface: ethers.utils.Interface; + try { + iface = new ethers.utils.Interface(artifact.abi as ethers.utils.Fragment[]); + } catch { + continue; + } + + for (const signature of Object.keys(iface.errors)) { + const selector = ethers.utils.id(signature).slice(0, 10); + if (!errors.has(selector)) { + errors.set(selector, { signature, sources: new Set() }); + } + errors.get(selector)!.sources.add(contractName); + } +} + +if (lookup) { + const hit = errors.get(lookup); + if (hit) { + console.log(`\n👉 ${lookup} ${hit.signature}`); + console.log(` defined in: ${[...hit.sources].sort().join(', ')}`); + } else { + console.log( + `\nNo error with selector ${lookup} found in ${errors.size} known errors.`, + ); + console.log('Did you run `yarn compile` first?'); + } + process.exit(hit ? 0 : 1); +} + +const rows = [...errors.entries()] + .map(([selector, { signature, sources }]) => ({ + selector, + signature, + sources: [...sources].sort().join(', '), + })) + .sort((a, b) => a.signature.localeCompare(b.signature)); + +const md = [ + '# Custom Errors', + '', + 'Auto-generated by `scripts/generate-errors-readme.ts` from compiled artifacts.', + `Total: ${rows.length} errors.`, + '', + '| Selector | Error | Declared in |', + '| --- | --- | --- |', + ...rows.map( + (r) => `| \`${r.selector}\` | \`${r.signature}\` | ${r.sources} |`, + ), + '', +].join('\n'); + +writeFileSync(outFile, md); +console.log(`Wrote ${rows.length} errors to ${outFile}`); diff --git a/scripts/upgrades/common/upgrade-contracts.ts b/scripts/upgrades/common/upgrade-contracts.ts index 05af2471..6a911976 100644 --- a/scripts/upgrades/common/upgrade-contracts.ts +++ b/scripts/upgrades/common/upgrade-contracts.ts @@ -19,9 +19,19 @@ import { import { getDeployer } from '../../deploy/common/utils'; // TODO: refactor this whole file and make upgrades more generic -type ContractType = 'customAggregator' | 'customAggregatorGrowth' | 'token'; +type ContractType = + | 'dataFeed' + | 'dataFeedComposite' + | 'dataFeedMultiply' + | 'customAggregator' + | 'customAggregatorGrowth' + | 'token'; -type ContractTypeToUpgrade = 'customFeed' | 'customFeedGrowth' | 'token'; +type ContractTypeToUpgrade = + | 'dataFeed' + | 'customFeed' + | 'customFeedGrowth' + | 'token'; type MTokenContractsToUpgrade = { mToken: MTokenName; @@ -30,11 +40,54 @@ type MTokenContractsToUpgrade = { contractType: ContractType; contractTypeTo?: ContractType; overrideImplementation?: string; + constructorArgs?: unknown[]; initializer?: string; initializerArgs?: unknown[]; }[]; }; +type ContractToUpgrade = { + mToken?: MTokenName; + contractType: string; + contractTypeTo?: string; + contractName: string; + proxyAddress: string; + overrideImplementation?: string; + initializer?: string; + initializerArgs?: unknown[]; + constructorArgs?: unknown[]; +}; + +export const proposeUpgradeContractsRaw = async ( + hre: HardhatRuntimeEnvironment, + upgradeId: string, + contractsToUpgrade: ContractToUpgrade[], +) => { + return upgradeAllContractsRaw( + hre, + upgradeId, + contractsToUpgrade, + async (hre, params, salt) => { + return await proposeTimeLockUpgradeTx(hre, params, salt); + }, + ); +}; + +export const executeUpgradeContractsRaw = async ( + hre: HardhatRuntimeEnvironment, + upgradeId: string, + contractsToUpgrade: ContractToUpgrade[], +) => { + return upgradeAllContractsRaw( + hre, + upgradeId, + contractsToUpgrade, + async (hre, params, salt) => { + return await executeTimeLockUpgradeTx(hre, params, salt); + }, + ); +}; + export const proposeUpgradeContracts = async ( hre: HardhatRuntimeEnvironment, upgradeId: string, @@ -56,7 +109,6 @@ export const executeUpgradeContracts = async ( hre: HardhatRuntimeEnvironment, upgradeId: string, contractType: ContractTypeToUpgrade, - mTokenContractsToUpgrade: MTokenContractsToUpgrade[], ) => { return upgradeAllContracts( @@ -112,16 +164,7 @@ const upgradeAllContracts = async ( console.log('mTokensToUpgrade', mTokenContractsToUpgrade); - const upgradeContracts: { - mToken: MTokenName; - contractType: ContractType; - contractTypeTo?: ContractType; - contractName: string; - proxyAddress: string; - overrideImplementation?: string; - initializer?: string; - initializerCalldata?: string; - }[] = []; + const contractsToUpgrade: ContractToUpgrade[] = []; for (const { mToken, contracts, addresses } of mTokenContractsToUpgrade) { for (const { @@ -130,6 +173,7 @@ const upgradeAllContracts = async ( overrideImplementation, initializer, initializerArgs, + constructorArgs, } of contracts) { const type = contractTypeTo ?? contractType; const contractName = @@ -139,12 +183,7 @@ const upgradeAllContracts = async ( throw new Error(`Contract name not found for ${mToken} ${type}`); } - const contract = await hre.ethers.getContractAt( - contractName, - addresses[contractTypeToUpgrade]!, - ); - - upgradeContracts.push({ + contractsToUpgrade.push({ mToken, contractType: type, contractTypeTo, @@ -152,19 +191,38 @@ const upgradeAllContracts = async ( proxyAddress: addresses[contractTypeToUpgrade]!, overrideImplementation, initializer, - initializerCalldata: initializer - ? contract.interface.encodeFunctionData( - initializer, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (initializerArgs ?? []) as readonly any[], - ) - : undefined, + initializerArgs: initializerArgs, + constructorArgs, }); } } + return await upgradeAllContractsRaw( + hre, + upgradeId, + contractsToUpgrade, + callBack, + ); +}; + +const upgradeAllContractsRaw = async ( + hre: HardhatRuntimeEnvironment, + upgradeId: string, + upgradeContracts: ContractToUpgrade[], + callBack: ( + hre: HardhatRuntimeEnvironment, + params: GetUpgradeTxParams, + salt: string, + ) => Promise, +) => { upgradeContracts.sort((a, b) => { - return a.mToken.localeCompare(b.mToken, 'en', { sensitivity: 'base' }); + return (a.mToken ?? a.contractName).localeCompare( + b.mToken ?? b.contractName, + 'en', + { + sensitivity: 'base', + }, + ); }); console.log('upgradeContracts', upgradeContracts); @@ -182,11 +240,14 @@ const upgradeAllContracts = async ( contractType, contractName, proxyAddress, + constructorArgs, } = upgradeContract; if (overrideImplementation) { console.log( - `Using override implementation (${overrideImplementation}) for ${mToken}`, + `Using override implementation (${overrideImplementation}) for ${ + mToken ? `${mToken} ${contractType}` : contractName + }`, ); continue; } @@ -199,13 +260,19 @@ const upgradeAllContracts = async ( { redeployImplementation: 'onchange', getTxResponse: true, + constructorArgs, }, ), ); if (deployedNew) { logDeploy(contractName, contractType, implementationAddress); - await etherscanVerify(hre, implementationAddress).catch((e) => { + await etherscanVerify( + hre, + implementationAddress, + contractName, + ...(constructorArgs ?? []), + ).catch((e) => { console.error('Verification failed', e); }); } else { @@ -221,8 +288,9 @@ const upgradeAllContracts = async ( } const failedUpgrades: { - mToken: MTokenName; - contractType: ContractType; + mToken?: MTokenName; + contractName: string; + contractType: string; error: string; }[] = []; @@ -233,13 +301,27 @@ const upgradeAllContracts = async ( Proxy: ${deployment.proxyAddress} Implementation: ${deployment.implementationAddress}`, ); + + const contract = await hre.ethers.getContractAt( + deployment.contractName, + deployment.proxyAddress, + ); + + const initializerCalldata = + deployment.initializerArgs && deployment.initializer + ? contract.interface.encodeFunctionData( + deployment.initializer, + deployment.initializerArgs, + ) + : undefined; + const result = await callBack( hre, { proxyAddress: deployment.proxyAddress, newImplementation: deployment.implementationAddress, initializer: deployment.initializer, - initializerCalldata: deployment.initializerCalldata, + initializerCalldata, }, upgradeId, ); @@ -252,6 +334,7 @@ Implementation: ${deployment.implementationAddress}`, failedUpgrades.push({ mToken: deployment.mToken, + contractName: deployment.contractName, contractType: deployment.contractType, error: e instanceof Error ? e.message : (e as string), }); diff --git a/scripts/upgrades/proposeUpgrade_Aggregators.ts b/scripts/upgrades/proposeUpgrade_Aggregators.ts deleted file mode 100644 index ef2707d9..00000000 --- a/scripts/upgrades/proposeUpgrade_Aggregators.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { parseUnits } from 'ethers/lib/utils'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; - -import { proposeUpgradeContracts } from './common/upgrade-contracts'; - -import { getCurrentAddresses } from '../../config/constants/addresses'; -import { getMTokenOrThrow } from '../../helpers/utils'; -import { DeployFunction } from '../deploy/common/types'; - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const upgradeId = 'mre7-custom-aggregator-upgrade-v3'; - - const networkAddresses = getCurrentAddresses(hre); - const mToken = getMTokenOrThrow(hre); - const tokenAddresses = networkAddresses?.[mToken]; - - if (!tokenAddresses) { - throw new Error('Token addresses not found'); - } - - await proposeUpgradeContracts(hre, upgradeId, 'customFeed', [ - { - mToken, - addresses: tokenAddresses, - contracts: [ - { - contractType: 'customAggregator', - initializer: 'initializeV2', - initializerArgs: [parseUnits('0.66', 8)], - }, - ], - }, - ]); -}; - -export default func; diff --git a/scripts/upgrades/upgrade_AccessControl.ts b/scripts/upgrades/upgrade_AccessControl.ts new file mode 100644 index 00000000..a4670eca --- /dev/null +++ b/scripts/upgrades/upgrade_AccessControl.ts @@ -0,0 +1,37 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +import { + executeUpgradeContractsRaw, + proposeUpgradeContractsRaw, +} from './common/upgrade-contracts'; + +import { getCurrentAddresses } from '../../config/constants/addresses'; +import { getCommonContractNames } from '../../helpers/contracts'; +import { getRolesForToken } from '../../helpers/roles'; +import { getActionOrThrow, upgradeActions } from '../../helpers/utils'; +import { DeployFunction } from '../deploy/common/types'; + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const upgradeId = 'q2-testnet-custom-aggregator-upgrade'; + + const networkAddresses = getCurrentAddresses(hre); + + const action = getActionOrThrow(hre, upgradeActions); + + const fn = + action === 'propose' + ? proposeUpgradeContractsRaw + : executeUpgradeContractsRaw; + + await fn(hre, upgradeId, [ + { + contractName: getCommonContractNames().ac, + proxyAddress: networkAddresses?.accessControl ?? '', + contractType: 'accessControl', + initializer: 'initializeV2', + initializerArgs: [0, [getRolesForToken('mGLOBAL').greenlisted]], + }, + ]); +}; + +export default func; diff --git a/scripts/upgrades/upgrade_Aggregators.ts b/scripts/upgrades/upgrade_Aggregators.ts new file mode 100644 index 00000000..8538045b --- /dev/null +++ b/scripts/upgrades/upgrade_Aggregators.ts @@ -0,0 +1,54 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +import { + executeUpgradeContractsRaw, + proposeUpgradeContractsRaw, +} from './common/upgrade-contracts'; + +import { MTokenName } from '../../config'; +import { getCurrentAddresses } from '../../config/constants/addresses'; +import { getRolesForToken } from '../../helpers/roles'; +import { getActionOrThrow, upgradeActions } from '../../helpers/utils'; +import { DeployFunction } from '../deploy/common/types'; + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const upgradeId = 'q2-testnet-custom-aggregator-upgrade'; + + const networkAddresses = getCurrentAddresses(hre); + const mTokens = ['mTBILL', 'mSL'] as MTokenName[]; + + const action = getActionOrThrow(hre, upgradeActions); + + const fn = + action === 'propose' + ? proposeUpgradeContractsRaw + : executeUpgradeContractsRaw; + + const types = ['customFeed', 'customFeedGrowth'] as const; + + const values = mTokens + .map((mToken) => { + return types + .map((type) => { + return { + mToken, + proxyAddress: networkAddresses?.[mToken]?.[type] ?? '', + contractType: + type === 'customFeed' + ? 'customAggregator' + : 'customAggregatorGrowth', + contractName: + type === 'customFeed' + ? 'CustomAggregatorV3CompatibleFeed' + : 'CustomAggregatorV3CompatibleFeedGrowth', + constructorArgs: [getRolesForToken(mToken).customFeedAdmin ?? ''], + }; + }) + .filter((v) => v.proxyAddress !== ''); + }) + .flat(); + + await fn(hre, upgradeId, values); +}; + +export default func; diff --git a/scripts/upgrades/upgrade_CustomAggregator.ts b/scripts/upgrades/upgrade_CustomAggregator.ts deleted file mode 100644 index 0d759ca2..00000000 --- a/scripts/upgrades/upgrade_CustomAggregator.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as hre from 'hardhat'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { DeployFunction } from 'hardhat-deploy/types'; - -import { getCurrentAddresses } from '../../config/constants/addresses'; -import { getTokenContractNames } from '../../helpers/contracts'; -import { getDeployer } from '../deploy/common/utils'; - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const addresses = getCurrentAddresses(hre); - - const deployer = await getDeployer(hre); - - const deployment = await hre.upgrades.upgradeProxy( - addresses?.mRE7SOL?.customFeed ?? '', - await hre.ethers.getContractFactory( - getTokenContractNames('mRE7SOL').customAggregator!, - deployer, - ), - { - redeployImplementation: 'onchange', - }, - ); - - if (typeof deployment !== 'string') { - await deployment.deployTransaction.wait(5); - console.log('deployment', deployment); - // console.log('deployment.to', deployment.to); - // await etherscanVerify(hre, deployment.to!); - } else { - console.log('deployment', deployment); - } -}; - -func(hre).then(console.log).catch(console.error); diff --git a/scripts/upgrades/upgrade_DV.ts b/scripts/upgrades/upgrade_DV.ts deleted file mode 100644 index a4cc120f..00000000 --- a/scripts/upgrades/upgrade_DV.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as hre from 'hardhat'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { DeployFunction } from 'hardhat-deploy/types'; - -import { getCurrentAddresses } from '../../config/constants/addresses'; -import { getTokenContractNames } from '../../helpers/contracts'; -import { getDeployer } from '../deploy/common/utils'; - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const addresses = getCurrentAddresses(hre); - - const deployer = await getDeployer(hre); - - const deployment = await hre.upgrades.prepareUpgrade( - addresses?.hbXAUt?.depositVault ?? '', - await hre.ethers.getContractFactory( - getTokenContractNames('hbXAUt').dv!, - deployer, - ), - { - redeployImplementation: 'onchange', - }, - ); - - if (typeof deployment !== 'string') { - await deployment.wait(5); - console.log(deployment.to); - } else { - console.log(deployment); - } -}; - -func(hre).then(console.log).catch(console.error); diff --git a/scripts/upgrades/upgrade_DataFeed.ts b/scripts/upgrades/upgrade_DataFeed.ts deleted file mode 100644 index f265fb1a..00000000 --- a/scripts/upgrades/upgrade_DataFeed.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { parseUnits } from 'ethers/lib/utils'; -import * as hre from 'hardhat'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { DeployFunction } from 'hardhat-deploy/types'; - -import { getCurrentAddresses } from '../../config/constants/addresses'; -import { getTokenContractNames } from '../../helpers/contracts'; -import { getDeployer } from '../deploy/common/utils'; - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const addresses = getCurrentAddresses(hre); - - const deployer = await getDeployer(hre); - - const deployment = await hre.upgrades.upgradeProxy( - addresses?.mTBILL?.dataFeed ?? '', - await hre.ethers.getContractFactory( - getTokenContractNames('mTBILL').dataFeed!, - deployer, - ), - { - call: { - fn: 'initializeV2', - args: [ - addresses?.accessControl ?? '', - addresses?.mTBILL?.customFeed ?? '', - hre.ethers.constants.MaxUint256, - parseUnits('0.1', 8), - parseUnits('1000', 8), - ], - }, - redeployImplementation: 'onchange', - }, - ); - - if (typeof deployment !== 'string') { - await deployment.deployTransaction.wait(5); - console.log('deployment', deployment); - // console.log('deployment.to', deployment.to); - // await etherscanVerify(hre, deployment.to!); - } else { - console.log('deployment', deployment); - } -}; - -func(hre).then(console.log).catch(console.error); diff --git a/scripts/upgrades/upgrade_Feeds.ts b/scripts/upgrades/upgrade_Feeds.ts new file mode 100644 index 00000000..cd6323f1 --- /dev/null +++ b/scripts/upgrades/upgrade_Feeds.ts @@ -0,0 +1,35 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +import { + executeUpgradeContractsRaw, + proposeUpgradeContractsRaw, +} from './common/upgrade-contracts'; + +import { getCurrentAddresses } from '../../config/constants/addresses'; +import { getAllRoles } from '../../helpers/roles'; +import { getActionOrThrow, upgradeActions } from '../../helpers/utils'; +import { DeployFunction } from '../deploy/common/types'; + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const upgradeId = 'q2-testnet-custom-aggregator-upgrade'; + + const networkAddresses = getCurrentAddresses(hre); + + const action = getActionOrThrow(hre, upgradeActions); + + const fn = + action === 'propose' + ? proposeUpgradeContractsRaw + : executeUpgradeContractsRaw; + + await fn(hre, upgradeId, [ + { + contractType: 'dataFeed', + contractName: 'DataFeed', + proxyAddress: networkAddresses?.paymentTokens?.usdc?.dataFeed ?? '', + constructorArgs: [getAllRoles().common.defaultAdmin], + }, + ]); +}; + +export default func; diff --git a/scripts/upgrades/upgrade_MTokens.ts b/scripts/upgrades/upgrade_MTokens.ts new file mode 100644 index 00000000..e0a708eb --- /dev/null +++ b/scripts/upgrades/upgrade_MTokens.ts @@ -0,0 +1,54 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +import { + executeUpgradeContracts, + proposeUpgradeContracts, +} from './common/upgrade-contracts'; + +import { MTokenName } from '../../config'; +import { getCurrentAddresses } from '../../config/constants/addresses'; +import { getRolesForToken } from '../../helpers/roles'; +import { getActionOrThrow, upgradeActions } from '../../helpers/utils'; +import { DeployFunction } from '../deploy/common/types'; +import { getDeployer } from '../deploy/common/utils'; + +const clawbackRecipients = {} as Record; + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const upgradeId = 'q2-testnet-custom-aggregator-upgrade'; + + const networkAddresses = getCurrentAddresses(hre); + const mTokens = ['mTBILL', 'mSL'] as MTokenName[]; + + const action = getActionOrThrow(hre, upgradeActions); + + const fn = + action === 'propose' ? proposeUpgradeContracts : executeUpgradeContracts; + + const deployer = await getDeployer(hre); + + await fn( + hre, + upgradeId, + 'token', + mTokens.map((mToken) => { + const clawbackRecipient = clawbackRecipients[mToken] ?? deployer.address; + const roles = getRolesForToken(mToken); + + return { + mToken, + addresses: networkAddresses?.[mToken] ?? {}, + contracts: [ + { + contractType: 'token', + initializer: 'initializeV2', + initializerArgs: [clawbackRecipient], + constructorArgs: [roles.tokenManager, roles.minter, roles.burner], + }, + ], + }; + }), + ); +}; + +export default func; diff --git a/scripts/upgrades/upgrade_RV.ts b/scripts/upgrades/upgrade_RV.ts deleted file mode 100644 index 5a2241b5..00000000 --- a/scripts/upgrades/upgrade_RV.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as hre from 'hardhat'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { DeployFunction } from 'hardhat-deploy/types'; - -import { getCurrentAddresses } from '../../config/constants/addresses'; -import { getTokenContractNames } from '../../helpers/contracts'; -import { getDeployer } from '../deploy/common/utils'; - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const addresses = getCurrentAddresses(hre); - - const deployer = await getDeployer(hre); - - const deployment = await hre.upgrades.upgradeProxy( - addresses?.mRE7SOL?.redemptionVault ?? '', - await hre.ethers.getContractFactory( - getTokenContractNames('mRE7SOL').rv!, - deployer, - ), - { - redeployImplementation: 'onchange', - }, - ); - - if (typeof deployment !== 'string') { - await deployment.deployTransaction.wait(5); - console.log('deployment', deployment); - // console.log('deployment.to', deployment.to); - // await etherscanVerify(hre, deployment.to!); - } else { - console.log('deployment', deployment); - } -}; - -func(hre).then(console.log).catch(console.error); diff --git a/scripts/upgrades/upgrade_RedemptionVaultBUIDL.ts b/scripts/upgrades/upgrade_RedemptionVaultBUIDL.ts deleted file mode 100644 index 24a43bc7..00000000 --- a/scripts/upgrades/upgrade_RedemptionVaultBUIDL.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as hre from 'hardhat'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { DeployFunction } from 'hardhat-deploy/types'; - -import { getCurrentAddresses } from '../../config/constants/addresses'; -import { getTokenContractNames } from '../../helpers/contracts'; -import { getDeployer } from '../deploy/common/utils'; - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const addresses = getCurrentAddresses(hre); - - const deployer = await getDeployer(hre); - - const deployment = await hre.upgrades.upgradeProxy( - addresses?.mTBILL?.redemptionVaultBuidl ?? '', - await hre.ethers.getContractFactory( - getTokenContractNames('mTBILL').rvBuidl!, - deployer, - ), - { - redeployImplementation: 'onchange', - }, - ); - - if (typeof deployment !== 'string') { - await deployment.deployTransaction.wait(5); - console.log(deployment.to); - } else { - console.log(deployment); - } -}; - -func(hre).then(console.log).catch(console.error); diff --git a/scripts/upgrades/upgrade_RedemptionVaultMToken.ts b/scripts/upgrades/upgrade_RedemptionVaultMToken.ts deleted file mode 100644 index 1943ca53..00000000 --- a/scripts/upgrades/upgrade_RedemptionVaultMToken.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types'; - -import { getCurrentAddresses } from '../../config/constants/addresses'; -import { getTokenContractNames } from '../../helpers/contracts'; -import { getMTokenOrThrow } from '../../helpers/utils'; -import { DeployFunction } from '../deploy/common/types'; -import { getDeployer } from '../deploy/common/utils'; - -/** - * Upgrades a RedemptionVaultWithSwapper proxy to use the - * RedemptionVaultWithMToken implementation. - * - * Usage: - * npx hardhat runscript scripts/upgrades/upgrade_RedemptionVaultMToken.ts --mtoken mFONE --network - * - * The script uses `prepareUpgrade` which validates storage layout - * compatibility and deploys the new implementation (if changed). - */ -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const mToken = getMTokenOrThrow(hre); - - const addresses = getCurrentAddresses(hre); - const proxyAddress = addresses?.[mToken]?.redemptionVaultSwapper; - - if (!proxyAddress) { - throw new Error( - `No redemptionVaultSwapper address found for ${mToken} on chain ${hre.network.config.chainId}`, - ); - } - - const deployer = await getDeployer(hre); - const contractName = getTokenContractNames(mToken).rvMToken; - - console.log( - `Upgrading ${mToken} Swapper proxy (${proxyAddress}) -> ${contractName}`, - ); - - const deployment = await hre.upgrades.prepareUpgrade( - proxyAddress, - await hre.ethers.getContractFactory(contractName, deployer), - { - redeployImplementation: 'onchange', - }, - ); - - if (typeof deployment !== 'string') { - await deployment.wait(5); - console.log('New implementation deployed at:', deployment.to); - } else { - console.log('Implementation address:', deployment); - } -}; - -export default func; diff --git a/scripts/upgrades/upgrade_RedemptionVaultSwapper.ts b/scripts/upgrades/upgrade_RedemptionVaultSwapper.ts deleted file mode 100644 index 27b850a3..00000000 --- a/scripts/upgrades/upgrade_RedemptionVaultSwapper.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as hre from 'hardhat'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { DeployFunction } from 'hardhat-deploy/types'; - -import { getCurrentAddresses } from '../../config/constants/addresses'; -import { getTokenContractNames } from '../../helpers/contracts'; -import { getDeployer } from '../deploy/common/utils'; - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const addresses = getCurrentAddresses(hre); - - const deployer = await getDeployer(hre); - - const deployment = await hre.upgrades.prepareUpgrade( - addresses?.hbXAUt?.redemptionVaultSwapper ?? '', - await hre.ethers.getContractFactory( - getTokenContractNames('hbXAUt').rvSwapper!, - deployer, - ), - { - redeployImplementation: 'onchange', - }, - ); - - if (typeof deployment !== 'string') { - await deployment.wait(5); - console.log(deployment.to); - } else { - console.log(deployment); - } -}; - -func(hre).then(console.log).catch(console.error); diff --git a/scripts/upgrades/upgrade_mToken.ts b/scripts/upgrades/upgrade_mToken.ts deleted file mode 100644 index 310715b5..00000000 --- a/scripts/upgrades/upgrade_mToken.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as hre from 'hardhat'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { DeployFunction } from 'hardhat-deploy/types'; - -import { getCurrentAddresses } from '../../config/constants/addresses'; -import { getTokenContractNames } from '../../helpers/contracts'; -import { getDeployer } from '../deploy/common/utils'; - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const addresses = getCurrentAddresses(hre); - - const deployer = await getDeployer(hre); - - const deployment = await hre.upgrades.upgradeProxy( - addresses?.mRE7SOL?.token ?? '', - await hre.ethers.getContractFactory( - getTokenContractNames('mRE7SOL').token!, - deployer, - ), - { - redeployImplementation: 'onchange', - }, - ); - - if (typeof deployment !== 'string') { - await deployment.deployTransaction.wait(5); - console.log('deployment', deployment); - // console.log('deployment.to', deployment.to); - // await etherscanVerify(hre, deployment.to!); - } else { - console.log('deployment', deployment); - } -}; - -func(hre).then(console.log).catch(console.error); diff --git a/test/common/ac.helpers.ts b/test/common/ac.helpers.ts index 2db77673..45eb8e89 100644 --- a/test/common/ac.helpers.ts +++ b/test/common/ac.helpers.ts @@ -1,12 +1,30 @@ +import { increase } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; +import { BigNumber, BigNumberish, constants, Contract } from 'ethers'; -import { Account, OptionalCommonParams, getAccount } from './common.helpers'; +import { + Account, + AccountOrContract, + OptionalCommonParams, + asyncForEach, + getAccount, + handleRevert, +} from './common.helpers'; +import { + executeTimelockOperationTester, + bulkScheduleTimelockOperationTester, +} from './timelock-manager.helpers'; +import { getAllRoles } from '../../helpers/roles'; +import { encodeFnSelector } from '../../helpers/utils'; import { Blacklistable, Greenlistable, + IMidasAccessControlManaged__factory, MidasAccessControl, + MidasAccessControlTimelockController, + MidasTimelockManager, } from '../../typechain-types'; type CommonParamsBlackList = { @@ -22,175 +40,804 @@ type CommonParamsGreenList = { owner: SignerWithAddress; }; +export const NULL_DELAY = 0; +// uint32 max value +export const NO_DELAY = BigNumber.from('0xFFFFFFFF'); + export const acErrors = { - WMAC_HASNT_ROLE: 'WMAC: hasnt role', - WMAC_HAS_ROLE: 'WMAC: has role', + WMAC_BLACKLISTED: (args?: unknown[], contract?: Contract) => ({ + contract, + customErrorName: 'Blacklisted', + args, + }), + WMAC_HASNT_PERMISSION: (args?: unknown[], contract?: Contract) => ({ + contract, + customErrorName: 'NoFunctionPermission', + args, + }), }; export const blackList = async ( + { blacklistable, accessControl, owner }: CommonParamsBlackList, + account: AccountOrContract, + opt?: OptionalCommonParams, +) => { + const allRoles = getAllRoles(); + await grantRoleTester( + { accessControl, owner }, + allRoles.common.blacklisted, + account, + 0, + opt, + ); +}; + +export const unBlackList = async ( { blacklistable, accessControl, owner }: CommonParamsBlackList, account: Account, opt?: OptionalCommonParams, ) => { account = getAccount(account); + const allRoles = getAllRoles(); - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( accessControl .connect(opt?.from ?? owner) - .grantRole(await blacklistable.BLACKLISTED_ROLE(), account), - ).revertedWith(opt?.revertMessage); + .revokeRole.bind(this, allRoles.common.blacklisted, account), + accessControl, + opt, + ) + ) { return; } await expect( accessControl .connect(opt?.from ?? owner) - .grantRole(await blacklistable.BLACKLISTED_ROLE(), account), + .revokeRole(allRoles.common.blacklisted, account), ).to.emit( accessControl, - accessControl.interface.events['RoleGranted(bytes32,address,address)'].name, - ).to.not.reverted; + accessControl.interface.events['RoleRevoked(bytes32,address,address)'].name, + ); - expect( - await accessControl.hasRole( - await accessControl.BLACKLISTED_ROLE(), - account, - ), - ).eq(true); + expect(await accessControl.hasRole(allRoles.common.blacklisted, account)).eq( + false, + ); }; -export const unBlackList = async ( - { blacklistable, accessControl, owner }: CommonParamsBlackList, +export const greenList = async ( + { greenlistable, accessControl, owner, role }: CommonParamsGreenList, + account: AccountOrContract, + opt?: OptionalCommonParams, +) => { + await grantRoleTester( + { accessControl, owner }, + role ?? (await greenlistable.greenlistedRole()), + account, + 0, + opt, + ); +}; + +export const unGreenList = async ( + { greenlistable, accessControl, owner, role }: CommonParamsGreenList, account: Account, opt?: OptionalCommonParams, ) => { - account = getAccount(account); + await revokeRoleTester( + { accessControl, owner }, + role ?? (await greenlistable.greenlistedRole()), + account, + opt, + ); +}; - if (opt?.revertMessage) { - await expect( - accessControl - .connect(opt?.from ?? owner) - .revokeRole(await blacklistable.BLACKLISTED_ROLE(), account), - ).revertedWith(opt?.revertMessage); +export const grantRoleMultTester = async ( + { + accessControl, + owner, + }: { + accessControl: MidasAccessControl; + owner: SignerWithAddress; + }, + params: { + role: string; + account: string; + delay?: BigNumberish; + }[], + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + + const callFn = accessControl.connect(from).grantRoleMult.bind( + this, + params.map((param) => ({ + ...param, + delay: param.delay ?? 0, + })), + ); + + if (await handleRevert(callFn, accessControl, opt)) { return; } - await expect( - accessControl - .connect(opt?.from ?? owner) - .revokeRole(await blacklistable.BLACKLISTED_ROLE(), account), - ).to.emit( + const hadRoles = await Promise.all( + params.map(({ role, account }) => accessControl.hasRole(role, account)), + ); + + const txPromise = callFn(); + let grantMultExpect: ReturnType | undefined; + for (const [index, { role, account }] of params.entries()) { + if (hadRoles[index]) { + continue; + } + + if (grantMultExpect === undefined) { + grantMultExpect = expect(txPromise) + .to.emit( + accessControl, + accessControl.interface.events['RoleGranted(bytes32,address,address)'] + .name, + ) + .withArgs(role, account, from.address); + } else { + grantMultExpect = grantMultExpect.to + .emit( + accessControl, + accessControl.interface.events['RoleGranted(bytes32,address,address)'] + .name, + ) + .withArgs(role, account, from.address); + } + } + + if (grantMultExpect !== undefined) { + await grantMultExpect; + } else { + await txPromise; + } + + await asyncForEach( + params.entries(), + async ([index, { account, role, delay }]) => { + expect(await accessControl.hasRole(role, account)).eq(true); + if (delay !== undefined && BigNumber.from(delay).gt(0)) { + const [actualDelay] = await accessControl.getRoleTimelockDelay(role, 0); + expect(actualDelay).eq(delay); + } + }, + ); +}; + +export const revokeRoleMultTester = async ( + { accessControl, - accessControl.interface.events['RoleRevoked(bytes32,address,address)'].name, - ).to.not.reverted; + owner, + }: { accessControl: MidasAccessControl; owner: SignerWithAddress }, + params: { + role: string; + account: string; + }[], + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + + const callFn = accessControl.connect(from).revokeRoleMult.bind(this, params); - expect( - await accessControl.hasRole( - await accessControl.BLACKLISTED_ROLE(), - account, - ), - ).eq(false); + if (await handleRevert(callFn, accessControl, opt)) { + return; + } + + const hadRoles = await Promise.all( + params.map(({ role, account }) => accessControl.hasRole(role, account)), + ); + + const txPromise = callFn(); + let revokeMultExpect: ReturnType | undefined; + for (const [index, { role, account }] of params.entries()) { + if (!hadRoles[index]) { + continue; + } + + if (revokeMultExpect === undefined) { + revokeMultExpect = expect(txPromise) + .to.emit( + accessControl, + accessControl.interface.events['RoleRevoked(bytes32,address,address)'] + .name, + ) + .withArgs(role, account, from.address); + } else { + revokeMultExpect = revokeMultExpect.to + .emit( + accessControl, + accessControl.interface.events['RoleRevoked(bytes32,address,address)'] + .name, + ) + .withArgs(role, account, from.address); + } + } + + if (revokeMultExpect !== undefined) { + await revokeMultExpect; + } else { + await txPromise; + } + + await asyncForEach(params, async ({ role, account }) => { + expect(await accessControl.hasRole(role, account)).eq(false); + }); }; -export const greenListToggler = async ( - { greenlistable, accessControl, owner, role }: CommonParamsGreenList, - account: Account, +export const grantRoleTester = async ( + { + accessControl, + owner, + }: { accessControl: MidasAccessControl; owner: SignerWithAddress }, + role: string, + account: AccountOrContract, + delay?: BigNumberish, opt?: OptionalCommonParams, ) => { + const from = opt?.from ?? owner; + account = getAccount(account); - if (opt?.revertMessage) { - await expect( - accessControl - .connect(opt?.from ?? owner) - .grantRole( - role ?? (await greenlistable.greenlistTogglerRole()), - account, - ), - ).revertedWith(opt?.revertMessage); + const callFn = + delay === undefined + ? accessControl + .connect(from) + ['grantRole(bytes32,address)'].bind(this, role, account) + : accessControl + .connect(from) + ['grantRole(bytes32,address,uint32)'].bind( + this, + role, + account, + delay, + ); + + if (await handleRevert(callFn, accessControl, opt)) { return; } - await expect( - accessControl - .connect(opt?.from ?? owner) - .grantRole(role ?? (await greenlistable.greenlistTogglerRole()), account), - ).to.emit( - accessControl, - accessControl.interface.events['RoleGranted(bytes32,address,address)'].name, - ).to.not.reverted; + const hadRole = await accessControl.hasRole(role, account); + + if (hadRole) { + await callFn(); + } else { + await expect(callFn()) + .to.emit( + accessControl, + accessControl.interface.events['RoleGranted(bytes32,address,address)'] + .name, + ) + .withArgs(role, account, from.address); + } - expect( - await accessControl.hasRole( - role ?? (await greenlistable.greenlistTogglerRole()), - account, - ), - ).eq(true); + expect(await accessControl.hasRole(role, account)).eq(true); + + if (delay !== undefined && BigNumber.from(delay).gt(0)) { + const [actualDelay] = await accessControl.getRoleTimelockDelay(role, 0); + expect(actualDelay).eq(delay); + } }; -export const greenList = async ( - { greenlistable, accessControl, owner, role }: CommonParamsGreenList, - account: Account, +export const revokeRoleTester = async ( + { + accessControl, + owner, + }: { accessControl: MidasAccessControl; owner: SignerWithAddress }, + role: string, + account: AccountOrContract, opt?: OptionalCommonParams, ) => { + const from = opt?.from ?? owner; + account = getAccount(account); + const callFn = accessControl + .connect(from) + .revokeRole.bind(this, role, account); - if (opt?.revertMessage) { - await expect( - accessControl - .connect(opt?.from ?? owner) - .grantRole(role ?? (await greenlistable.GREENLISTED_ROLE()), account), - ).revertedWith(opt?.revertMessage); + if (await handleRevert(callFn, accessControl, opt)) { return; } - await expect( - accessControl - .connect(opt?.from ?? owner) - .grantRole(role ?? (await greenlistable.GREENLISTED_ROLE()), account), - ).to.emit( + const hadRole = await accessControl.hasRole(role, account); + + if (!hadRole) { + await callFn(); + } else { + await expect(callFn()) + .to.emit( + accessControl, + accessControl.interface.events['RoleRevoked(bytes32,address,address)'] + .name, + ) + .withArgs(role, account, from.address); + } + + expect(await accessControl.hasRole(role, account)).eq(false); +}; + +export const setIsUserFacingRoleTester = async ( + { accessControl, - accessControl.interface.events['RoleGranted(bytes32,address,address)'].name, - ).to.not.reverted; + owner, + }: { accessControl: MidasAccessControl; owner: SignerWithAddress }, + params: { role: string; enabled: boolean }[], + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; - expect( - await accessControl.hasRole( - role ?? (await accessControl.GREENLISTED_ROLE()), - account, - ), - ).eq(true); + const callFn = accessControl + .connect(from) + .setUserFacingRoleMult.bind(this, params); + + if (await handleRevert(callFn, accessControl, opt)) { + return; + } + + const statesBefore = await Promise.all( + params.map(async (param) => { + return await accessControl.isUserFacingRole(param.role); + }), + ); + + const txPromise = callFn(); + let userFacingExpect: ReturnType | undefined; + for (const [index, stateBefore] of statesBefore.entries()) { + const param = params[index]; + if (stateBefore !== param.enabled) { + if (userFacingExpect === undefined) { + userFacingExpect = expect(txPromise) + .to.emit( + accessControl, + accessControl.interface.events['SetUserFacingRole(bytes32,bool)'] + .name, + ) + .withArgs(param.role, param.enabled); + } else { + userFacingExpect = userFacingExpect.to + .emit( + accessControl, + accessControl.interface.events['SetUserFacingRole(bytes32,bool)'] + .name, + ) + .withArgs(param.role, param.enabled); + } + } + } + if (userFacingExpect !== undefined) { + await userFacingExpect; + } else { + await txPromise; + } + + await asyncForEach(params, async (param) => { + expect(await accessControl.isUserFacingRole(param.role)).eq(param.enabled); + }); }; -export const unGreenList = async ( - { greenlistable, accessControl, owner, role }: CommonParamsGreenList, - account: Account, +export const setGrantOperatorRoleTester = async ( + { + accessControl, + owner, + }: { accessControl: MidasAccessControl; owner: SignerWithAddress }, + targetContract: string, + params: { + functionSelector: string; + operator: string; + enabled: boolean; + delay?: BigNumberish; + }[], opt?: OptionalCommonParams, ) => { - account = getAccount(account); + const from = opt?.from ?? owner; - if (opt?.revertMessage) { - await expect( - accessControl - .connect(opt?.from ?? owner) - .revokeRole(role ?? (await greenlistable.GREENLISTED_ROLE()), account), - ).revertedWith(opt?.revertMessage); + const callFn = accessControl.connect(from).setGrantOperatorRoleMult.bind( + this, + targetContract, + params.map((param) => ({ + ...param, + delay: param.delay ?? 0, + })), + ); + + const masterRole = params.length + ? await IMidasAccessControlManaged__factory.connect( + targetContract, + accessControl.provider, + ).contractAdminRole() + : constants.HashZero; + + if (await handleRevert(callFn, accessControl, opt)) { return; } - await expect( - accessControl - .connect(opt?.from ?? owner) - .revokeRole(role ?? (await greenlistable.GREENLISTED_ROLE()), account), - ).to.emit( + const statesBefore = await Promise.all( + params.map(async (param) => { + return await accessControl[ + 'isFunctionAccessGrantOperator(bytes32,address,bytes4,address)' + ](masterRole, targetContract, param.functionSelector, param.operator); + }), + ); + + const txPromise = callFn(); + let grantOperatorExpect: ReturnType | undefined; + for (const [index, stateBefore] of statesBefore.entries()) { + const param = params[index]; + + if (stateBefore !== param.enabled) { + if (grantOperatorExpect === undefined) { + grantOperatorExpect = expect(txPromise) + .to.emit( + accessControl, + accessControl.interface.events[ + 'SetGrantOperatorRole(bytes32,address,address,bytes4,bool)' + ].name, + ) + .withArgs( + masterRole, + targetContract, + param.operator, + param.functionSelector, + param.enabled, + ); + } else { + grantOperatorExpect = grantOperatorExpect.to + .emit( + accessControl, + accessControl.interface.events[ + 'SetGrantOperatorRole(bytes32,address,address,bytes4,bool)' + ].name, + ) + .withArgs( + masterRole, + targetContract, + param.operator, + param.functionSelector, + param.enabled, + ); + } + } + } + if (grantOperatorExpect !== undefined) { + await grantOperatorExpect; + } else { + await txPromise; + } + + await asyncForEach(statesBefore.entries(), async ([index, stateBefore]) => { + const param = params[index]; + + expect( + await accessControl[ + 'isFunctionAccessGrantOperator(bytes32,address,bytes4,address)' + ](masterRole, targetContract, param.functionSelector, param.operator), + ).eq(param.enabled); + + if (param.delay !== undefined && BigNumber.from(param.delay).gt(0)) { + const operatorKey = await accessControl.grantOperatorRoleKey( + masterRole, + targetContract, + param.functionSelector, + ); + const [actualDelay] = await accessControl.getRoleTimelockDelay( + operatorKey, + 0, + ); + expect(actualDelay).eq(param.delay); + } + }); +}; + +export const setPermissionRoleTester = async ( + { accessControl, - accessControl.interface.events['RoleRevoked(bytes32,address,address)'].name, - ).to.not.reverted; - - expect( - await accessControl.hasRole( - role ?? (await accessControl.GREENLISTED_ROLE()), - account, - ), - ).eq(false); + owner, + }: { + accessControl: MidasAccessControl; + owner: SignerWithAddress; + }, + masterRole: string | undefined, + targetContract: string, + functionSelector: string, + params: { + account: string; + enabled: boolean; + }[], + delay?: BigNumberish, + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + + const callFn = masterRole + ? accessControl + .connect(from) + [ + 'setPermissionRoleMult(bytes32,address,bytes4,uint32,(address,bool)[])' + ].bind( + this, + masterRole, + targetContract, + functionSelector, + delay ?? 0, + params, + ) + : accessControl + .connect(from) + ['setPermissionRoleMult(address,bytes4,uint32,(address,bool)[])'].bind( + this, + targetContract, + functionSelector, + delay ?? 0, + params, + ); + + if (await handleRevert(callFn, accessControl, opt)) { + return; + } + + masterRole = + masterRole ?? + (await IMidasAccessControlManaged__factory.connect( + targetContract, + accessControl.provider, + ).contractAdminRole()); + + const statesBefore = await Promise.all( + params.map(async (param) => { + return await accessControl[ + 'hasFunctionPermission(bytes32,address,bytes4,address)' + ](masterRole, targetContract, functionSelector, param.account); + }), + ); + + const txPromise = callFn(); + let permissionExpect: ReturnType | undefined; + for (const [index, stateBefore] of statesBefore.entries()) { + const param = params[index]; + + if (stateBefore !== param.enabled) { + if (permissionExpect === undefined) { + permissionExpect = expect(txPromise) + .to.emit( + accessControl, + accessControl.interface.events[ + 'SetPermissionRole(bytes32,address,address,bytes4,bool)' + ].name, + ) + .withArgs( + masterRole, + targetContract, + param.account, + functionSelector, + param.enabled, + ); + } else { + permissionExpect = permissionExpect.to + .emit( + accessControl, + accessControl.interface.events[ + 'SetPermissionRole(bytes32,address,address,bytes4,bool)' + ].name, + ) + .withArgs( + masterRole, + targetContract, + param.account, + functionSelector, + param.enabled, + ); + } + } + } + if (permissionExpect !== undefined) { + await permissionExpect; + } else { + await txPromise; + } + + const key = await accessControl.permissionRoleKey( + masterRole, + targetContract, + functionSelector, + ); + + if (delay !== undefined && BigNumber.from(delay).gt(0)) { + const [actualDelay] = await accessControl.getRoleTimelockDelay(key, 0); + expect(actualDelay).eq(delay); + } + + await asyncForEach(statesBefore.entries(), async ([index, stateBefore]) => { + const param = params[index]; + + expect( + await accessControl[ + 'hasFunctionPermission(bytes32,address,bytes4,address)' + ](masterRole, targetContract, functionSelector, param.account), + ).eq(param.enabled); + }); +}; + +type SetupFunctionAccessGrantOperatorParams = { + accessControl: MidasAccessControl; + owner: SignerWithAddress; + // TODO: remove it + masterRole: string; + targetContract: string; + functionSelector: string; + grantOperator: SignerWithAddress; +}; + +export const setupGrantOperatorRole = async ({ + accessControl, + owner, + targetContract, + functionSelector, + grantOperator, +}: SetupFunctionAccessGrantOperatorParams) => { + await setGrantOperatorRoleTester({ accessControl, owner }, targetContract, [ + { + functionSelector, + operator: grantOperator.address, + enabled: true, + }, + ]); +}; + +export const setupPermissionRole = async ( + { + accessControl, + owner, + }: { accessControl: MidasAccessControl; owner: SignerWithAddress }, + vaultRole: string, + vaultAddress: string, + functionSignature: string, + account: string, +) => { + const selector = encodeFnSelector(functionSignature); + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: vaultRole, + targetContract: vaultAddress, + functionSelector: selector, + grantOperator: owner, + }); + await setPermissionRoleTester( + { accessControl, owner }, + vaultRole, + vaultAddress, + selector, + [ + { + account, + enabled: true, + }, + ], + ); +}; +type CommonParamsAccessControl = { + timelockManager: MidasTimelockManager; + accessControl: MidasAccessControl; + owner: SignerWithAddress; + timelock: MidasAccessControlTimelockController; +}; + +// TODO: refactor, role and delays should be an array of objects +export const setRoleTimelocksTester = async ( + { accessControl, owner }: CommonParamsAccessControl, + roles: string[], + delays: BigNumberish[], + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + + const params = roles.map((role, index) => ({ + role, + delay: delays[index], + })); + + const callFn = accessControl + .connect(from) + .setRoleDelayMult.bind(this, params); + + if (await handleRevert(callFn, accessControl, opt)) { + return; + } + + await expect(callFn()) + .to.emit(accessControl, 'SetRoleDelays') + .withArgs((actualParams: { role: string; delay: BigNumberish }[]) => { + expect(actualParams.length).eq(params.length); + params.forEach((param, index) => { + expect(actualParams[index].role).eq(param.role); + expect(actualParams[index].delay).eq(param.delay); + }); + return true; + }); + + await asyncForEach(roles.entries(), async ([index, role]) => { + const delayParam = delays[index]; + const [delay, isDefault] = await accessControl.getRoleTimelockDelay( + role, + 0, + ); + const expectedDelay = BigNumber.from(0).eq(delayParam) + ? 3600 + : NO_DELAY.eq(delayParam) + ? 0 + : delayParam; + + expect(delay).eq(expectedDelay); + expect(isDefault).eq(BigNumber.from(0).eq(delayParam)); + }); +}; + +export const setRoleTimelocksAndExecute = async ( + { + owner, + accessControl, + timelock, + timelockManager, + }: CommonParamsAccessControl, + params: { + role: string; + delay: BigNumberish; + }[], + opt?: OptionalCommonParams, +) => { + const [delay] = await accessControl.getRoleTimelockDelay( + constants.HashZero, + 0, + ); + + const data = accessControl.interface.encodeFunctionData('setRoleDelayMult', [ + params, + ]); + + const from = opt?.from ?? owner; + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [data], + { isSetCouncilOperation: false }, + { from }, + ); + await increase(delay + 1); + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + data, + from.address, + { from }, + ); +}; + +export const setDefaultDelayTest = async ( + { accessControl, owner }: CommonParamsAccessControl, + defaultDelay: BigNumberish, + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + const callFn = accessControl + .connect(from) + .setDefaultDelay.bind(this, defaultDelay); + + if (await handleRevert(callFn, accessControl, opt)) { + return; + } + + await expect(callFn()) + .to.emit( + accessControl, + accessControl.interface.events['SetDefaultDelay(uint32)'].name, + ) + .withArgs(defaultDelay); + + expect(await accessControl.defaultDelay()).to.eq(defaultDelay); }; diff --git a/test/common/axelar.helpers.ts b/test/common/axelar.helpers.ts index 63bf0f0a..72c6dacf 100644 --- a/test/common/axelar.helpers.ts +++ b/test/common/axelar.helpers.ts @@ -1,9 +1,13 @@ import { expect } from 'chai'; -import { BigNumberish, constants, Contract } from 'ethers'; +import { BigNumberish, constants } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; -import { OptionalCommonParams, tokenAmountToBase18 } from './common.helpers'; +import { + handleRevert, + OptionalCommonParams, + tokenAmountToBase18, +} from './common.helpers'; import { calcExpectedMintAmount } from './deposit-vault.helpers'; import { axelarFixture } from './fixtures'; import { calcExpectedTokenOutAmount } from './redemption-vault.helpers'; @@ -51,10 +55,6 @@ export const sendIts = async ( }, opt?: { revertOnDst?: boolean; - revertWithCustomError?: { - contract: Contract; - error: string; - }; } & OptionalCommonParams, ) => { const from = opt?.from ?? owner; @@ -73,27 +73,19 @@ export const sendIts = async ( const itsFrom = direction === 'A_TO_B' ? axelarItsA : axelarItsB; - const params = [ - mTokenId, - direction === 'A_TO_B' ? chainNameB : chainNameA, - recipient, - amountParsed, - ] as const; - - if (opt?.revertMessage && !opt?.revertOnDst) { - await expect( - itsFrom.connect(opt?.from ?? owner).interchainTransfer(...params), - ).revertedWith(opt?.revertMessage); - return; - } - - if (opt?.revertWithCustomError && !opt?.revertOnDst) { - await expect( - itsFrom.connect(opt?.from ?? owner).interchainTransfer(...params), - ).revertedWithCustomError( - opt?.revertWithCustomError.contract, - opt?.revertWithCustomError.error, + const callFn = itsFrom + .connect(opt?.from ?? owner) + .interchainTransfer.bind( + this, + mTokenId, + direction === 'A_TO_B' ? chainNameB : chainNameA, + recipient, + amountParsed, + [], + 0, ); + + if (!opt?.revertOnDst && (await handleRevert(callFn, itsFrom, opt))) { return; } @@ -101,8 +93,10 @@ export const sendIts = async ( const balanceFromBefore = await mTBILL.balanceOf(from.address); const balanceToBefore = await mTBILL.balanceOf(recipient); - await expect(itsFrom.connect(from).interchainTransfer(...params)).not - .reverted; + await expect(callFn()).to.emit( + mTBILL, + mTBILL.interface.events['Transfer(address,address,uint256)'].name, + ); const totalSupplyAfter = await mTBILL.totalSupply(); const balanceFromAfter = await mTBILL.balanceOf(from.address); @@ -152,12 +146,7 @@ export const depositAndSend = async ( referrerId?: string; minReceiveAmount?: BigNumberish; }, - opt?: { - revertWithCustomError?: { - contract: Contract; - error: string; - }; - } & OptionalCommonParams, + opt?: OptionalCommonParams, ) => { const from = opt?.from ?? owner; recipient ??= from.address; @@ -200,20 +189,11 @@ export const depositAndSend = async ( ); const params = [amountParsed, encodedData] as const; - if (opt?.revertMessage) { - await expect( - executor.connect(opt?.from ?? owner).depositAndSend(...params), - ).revertedWith(opt?.revertMessage); - return; - } + const callFn = executor + .connect(opt?.from ?? owner) + .depositAndSend.bind(this, ...params); - if (opt?.revertWithCustomError) { - await expect( - executor.connect(opt?.from ?? owner).depositAndSend(...params), - ).revertedWithCustomError( - opt?.revertWithCustomError.contract, - opt?.revertWithCustomError.error, - ); + if (await handleRevert(callFn, executor, opt)) { return; } @@ -221,7 +201,7 @@ export const depositAndSend = async ( const balanceFromBefore = await pToken.balanceOf(from.address); const balanceToBefore = await mTBILL.balanceOf(recipient); - await expect(executor.connect(from).depositAndSend(...params)) + await expect(callFn()) .to.emit( executor, executor.interface.events['Deposited(bytes,bytes,string,uint256,uint256)'] @@ -265,12 +245,7 @@ export const redeemAndSend = async ( direction?: 'A_TO_A' | 'B_TO_B' | 'B_TO_A' | 'A_TO_B'; minReceiveAmount?: BigNumberish; }, - opt?: { - revertWithCustomError?: { - contract: Contract; - error: string; - }; - } & OptionalCommonParams, + opt?: OptionalCommonParams, ) => { const from = opt?.from ?? owner; recipient ??= from.address; @@ -306,18 +281,9 @@ export const redeemAndSend = async ( ); const params = [amountParsed, encodedData] as const; - const txFn = executor.connect(from).redeemAndSend.bind(this, ...params); - - if (opt?.revertMessage) { - await expect(txFn()).revertedWith(opt?.revertMessage); - return; - } + const callFn = executor.connect(from).redeemAndSend.bind(this, ...params); - if (opt?.revertWithCustomError) { - await expect(txFn()).revertedWithCustomError( - opt?.revertWithCustomError.contract, - opt?.revertWithCustomError.error, - ); + if (await handleRevert(callFn, executor, opt)) { return; } @@ -325,7 +291,7 @@ export const redeemAndSend = async ( const balanceFromBefore = await mTBILL.balanceOf(from.address); const balanceToBefore = await pToken.balanceOf(recipient); - await expect(txFn()) + await expect(callFn()) .to.emit( executor, executor.interface.events['Redeemed(bytes,bytes,string,uint256,uint256)'] diff --git a/test/common/common.helpers.ts b/test/common/common.helpers.ts index ca7c9f3f..10d358ec 100644 --- a/test/common/common.helpers.ts +++ b/test/common/common.helpers.ts @@ -1,22 +1,49 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { BigNumber, BigNumberish, Contract } from 'ethers'; +import { + BigNumber, + BigNumberish, + Contract, + ContractFactory, + ContractTransaction, +} from 'ethers'; import { parseUnits, solidityKeccak256 } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; +import { DefaultFixture } from './fixtures'; + import { ERC20, + ERC20__factory, ERC20Mock, IERC20Metadata, - MTBILL, - Pausable, + MidasPauseManager, + MToken, + USTBMock, } from '../../typechain-types'; - -export type OptionalCommonParams = { - from?: SignerWithAddress; - revertMessage?: string; +type RevertCustomError = { + contract?: Contract; + customErrorName: string; + args?: unknown[]; }; +export type OptionalCommonParams = + | { + from?: SignerWithAddress; + revertMessage?: string; + } + | { + from?: SignerWithAddress; + revertCustomError: RevertCustomError; + } + | { + from?: SignerWithAddress; + revertCustomError: ( + args?: unknown[], + contract?: Contract, + ) => RevertCustomError; + }; + export type Account = SignerWithAddress | string; export type AccountOrContract = Account | Contract; @@ -24,6 +51,50 @@ export const keccak256 = (role: string) => { return solidityKeccak256(['string'], [role]); }; +export const shouldRevert = (opt?: OptionalCommonParams) => { + return ( + opt && + (('revertMessage' in opt && opt.revertMessage) || + ('revertCustomError' in opt && opt.revertCustomError)) + ); +}; + +export const handleRevert = async ( + txOrTxFn: (() => Promise) | Promise, + contract: Contract, + opt?: OptionalCommonParams, +) => { + if (!opt || !shouldRevert(opt)) return false; + + const getPromise = () => + typeof txOrTxFn === 'function' ? txOrTxFn() : txOrTxFn; + + if ('revertCustomError' in opt && opt.revertCustomError) { + const txPromise = getPromise(); + const revertCustomError = + typeof opt.revertCustomError === 'function' + ? opt.revertCustomError(undefined, contract) + : opt.revertCustomError; + + const match = expect(txPromise).revertedWithCustomError( + revertCustomError.contract ?? contract, + revertCustomError.customErrorName, + ); + + await (revertCustomError.args + ? match.withArgs(...revertCustomError.args) + : match); + + return true; + } else if ('revertMessage' in opt && opt.revertMessage) { + const txPromise = getPromise(); + await expect(txPromise).revertedWith(opt.revertMessage); + return true; + } else { + return false; + } +}; + export const getAccount = (account: AccountOrContract) => { return ( (account as SignerWithAddress).address ?? @@ -32,88 +103,411 @@ export const getAccount = (account: AccountOrContract) => { ); }; +type PauseParams = { + pauseManager: MidasPauseManager; + owner: SignerWithAddress; +}; + +export const pauseGlobalTest = async ( + { pauseManager, owner }: PauseParams, + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + + const callFn = pauseManager.connect(from).globalPause.bind(this); + + if (await handleRevert(callFn, pauseManager, opt)) { + return; + } + + const wasPaused = await pauseManager.globalPaused(); + + if (wasPaused) { + await callFn(); + } else { + await expect(callFn()) + .to.emit( + pauseManager, + pauseManager.interface.events['GlobalPauseStatusChange(bool)'].name, + ) + .withArgs(true); + } + + expect(await pauseManager.globalPaused()).eq(true); +}; + +export const unpauseGlobalTest = async ( + { pauseManager, owner }: PauseParams, + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + + if ( + await handleRevert( + pauseManager.connect(from).globalUnpause.bind(this), + pauseManager, + opt, + ) + ) { + return; + } + + const wasPaused = await pauseManager.globalPaused(); + + if (!wasPaused) { + await pauseManager.connect(from).globalUnpause(); + } else { + await expect(pauseManager.connect(from).globalUnpause()) + .to.emit( + pauseManager, + pauseManager.interface.events['GlobalPauseStatusChange(bool)'].name, + ) + .withArgs(false); + } + + expect(await pauseManager.globalPaused()).eq(false); +}; + +// TODO: rename to pauseContracts export const pauseVault = async ( - vault: Pausable, + { pauseManager, owner }: PauseParams, + contracts: Contract | Contract[], opt?: OptionalCommonParams, ) => { - const [defaultSigner] = await ethers.getSigners(); + const from = opt?.from ?? owner; - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? defaultSigner).pause(), - ).revertedWith(opt?.revertMessage); + const contractsArr = Array.isArray(contracts) ? contracts : [contracts]; + + if ( + await handleRevert( + pauseManager.connect(from).bulkPauseContract.bind( + this, + contractsArr.map((c) => c.address), + ), + pauseManager, + opt, + ) + ) { return; } - await expect(await vault.connect(opt?.from ?? defaultSigner).pause()).not - .reverted; + const contractAddresses = contractsArr.map((c) => c.address); + const addressesToPause = ( + await Promise.all( + contractAddresses.map(async (contractAddr) => ({ + contractAddr, + paused: await pauseManager.contractPaused(contractAddr), + })), + ) + ) + .filter(({ paused }) => !paused) + .map(({ contractAddr }) => contractAddr); + + const tx = pauseManager.connect(from).bulkPauseContract(contractAddresses); + if (addressesToPause.length > 0) { + let pauseExpect = expect(tx); + for (const contractAddr of addressesToPause) { + pauseExpect = pauseExpect.to + .emit( + pauseManager, + pauseManager.interface.events[ + 'ContractPauseStatusChange(address,bool)' + ].name, + ) + .withArgs(contractAddr, true); + } + await pauseExpect; + } else { + await tx; + } - expect(await vault.paused()).eq(true); + await asyncForEach(contractsArr, async (contract) => { + expect(await pauseManager.isPaused(contract.address, '0x00000000')).eq( + true, + ); + expect(await pauseManager.contractPaused(contract.address)).eq(true); + }); }; +// TODO: rename to unpauseContracts +export const unpauseVault = async ( + { owner, pauseManager }: PauseParams, + contracts: Contract | Contract[], + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + + const contractsArr = Array.isArray(contracts) ? contracts : [contracts]; + + if ( + await handleRevert( + pauseManager.connect(from).bulkUnpauseContract.bind( + this, + contractsArr.map((c) => c.address), + ), + pauseManager, + opt, + ) + ) { + return; + } + + const contractAddresses = contractsArr.map((c) => c.address); + const addressesToUnpause = ( + await Promise.all( + contractAddresses.map(async (contractAddr) => ({ + contractAddr, + paused: await pauseManager.contractPaused(contractAddr), + })), + ) + ) + .filter(({ paused }) => paused) + .map(({ contractAddr }) => contractAddr); + + const tx = pauseManager.connect(from).bulkUnpauseContract(contractAddresses); + if (addressesToUnpause.length > 0) { + let unpauseExpect = expect(tx); + for (const contractAddr of addressesToUnpause) { + unpauseExpect = unpauseExpect.to + .emit( + pauseManager, + pauseManager.interface.events[ + 'ContractPauseStatusChange(address,bool)' + ].name, + ) + .withArgs(contractAddr, false); + } + await unpauseExpect; + } else { + await tx; + } + + await asyncForEach(contractsArr, async (contract) => { + expect(await pauseManager.isPaused(contract.address, '0x00000000')).eq( + false, + ); + expect(await pauseManager.contractPaused(contract.address)).eq(false); + }); +}; + +// TODO: rename to pauseContractsFn export const pauseVaultFn = async ( - vault: Pausable, - fnSelector: string, + { pauseManager, owner }: PauseParams, + contracts: Contract | Contract[], + fnSelector: string | string[], opt?: OptionalCommonParams, ) => { - const [defaultSigner] = await ethers.getSigners(); + const from = opt?.from ?? owner; - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? defaultSigner).pauseFn(fnSelector), - ).revertedWith(opt?.revertMessage); + const selectors = Array.isArray(fnSelector) ? fnSelector : [fnSelector]; + + const contractsArr = Array.isArray(contracts) ? contracts : [contracts]; + + if ( + await handleRevert( + pauseManager.connect(from).bulkPauseContractFn.bind( + this, + contractsArr.map((c) => c.address), + selectors, + ), + pauseManager, + opt, + ) + ) { return; } - await expect( - await vault.connect(opt?.from ?? defaultSigner).pauseFn(fnSelector), - ).not.reverted; + const contractAddresses = contractsArr.map((c) => c.address); + const fnPauseTargets = ( + await Promise.all( + contractAddresses.flatMap((contractAddr) => + selectors.map(async (fnSelector) => ({ + contractAddr, + fnSelector, + paused: await pauseManager.isFunctionPaused(contractAddr, fnSelector), + })), + ), + ) + ).filter(({ paused }) => !paused); + + const tx = pauseManager + .connect(from) + .bulkPauseContractFn(contractAddresses, selectors); + if (fnPauseTargets.length > 0) { + let pauseFnExpect = expect(tx); + for (const { contractAddr, fnSelector } of fnPauseTargets) { + pauseFnExpect = pauseFnExpect.to + .emit( + pauseManager, + pauseManager.interface.events[ + 'FnPauseStatusChange(address,bytes4,bool)' + ].name, + ) + .withArgs(contractAddr, fnSelector, true); + } + await pauseFnExpect; + } else { + await tx; + } - expect(await vault.fnPaused(fnSelector)).eq(true); + await asyncForEach(contractsArr, async (contract) => { + await asyncForEach(selectors, async (fnSelector) => { + expect(await pauseManager.isPaused(contract.address, fnSelector)).eq( + true, + ); + expect( + await pauseManager.contractFnPaused(contract.address, fnSelector), + ).eq(true); + }); + }); }; +// TODO: rename to unpauseContractsFn export const unpauseVaultFn = async ( - vault: Pausable, - fnSelector: string, + { pauseManager, owner }: PauseParams, + contracts: Contract | Contract[], + fnSelector: string | string[], opt?: OptionalCommonParams, ) => { - const [defaultSigner] = await ethers.getSigners(); + const from = opt?.from ?? owner; + + const selectors = Array.isArray(fnSelector) ? fnSelector : [fnSelector]; - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? defaultSigner).unpauseFn(fnSelector), - ).revertedWith(opt?.revertMessage); + const contractsArr = Array.isArray(contracts) ? contracts : [contracts]; + if ( + await handleRevert( + pauseManager.connect(from).bulkUnpauseContractFn.bind( + this, + contractsArr.map((c) => c.address), + selectors, + ), + pauseManager, + opt, + ) + ) { return; } - await expect( - await vault.connect(opt?.from ?? defaultSigner).unpauseFn(fnSelector), - ).not.reverted; + const contractAddresses = contractsArr.map((c) => c.address); + const fnUnpauseTargets = ( + await Promise.all( + contractAddresses.flatMap((contractAddr) => + selectors.map(async (fnSelector) => ({ + contractAddr, + fnSelector, + paused: await pauseManager.isFunctionPaused(contractAddr, fnSelector), + })), + ), + ) + ).filter(({ paused }) => paused); + + const tx = pauseManager + .connect(from) + .bulkUnpauseContractFn(contractAddresses, selectors); + if (fnUnpauseTargets.length > 0) { + let unpauseFnExpect = expect(tx); + for (const { contractAddr, fnSelector } of fnUnpauseTargets) { + unpauseFnExpect = unpauseFnExpect.to + .emit( + pauseManager, + pauseManager.interface.events[ + 'FnPauseStatusChange(address,bytes4,bool)' + ].name, + ) + .withArgs(contractAddr, fnSelector, false); + } + await unpauseFnExpect; + } else { + await tx; + } - expect(await vault.fnPaused(fnSelector)).eq(false); + await asyncForEach(contractsArr, async (contract) => { + await asyncForEach(selectors, async (fnSelector) => { + expect(await pauseManager.isPaused(contract.address, fnSelector)).eq( + false, + ); + expect( + await pauseManager.contractFnPaused(contract.address, fnSelector), + ).eq(false); + }); + }); }; -export const unpauseVault = async ( - vault: Pausable, +export const adminPauseContractTest = async ( + { pauseManager, owner }: PauseParams, + contract: Contract, opt?: OptionalCommonParams, ) => { - const [defaultSigner] = await ethers.getSigners(); + const from = opt?.from ?? owner; - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? defaultSigner).unpause(), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + pauseManager + .connect(from) + .contractAdminPause.bind(this, contract.address), + pauseManager, + opt, + ) + ) { return; } + const alreadyPaused = await pauseManager.contractPaused(contract.address); + const tx = pauseManager.connect(from).contractAdminPause(contract.address); - await expect(await vault.connect(opt?.from ?? defaultSigner).unpause()).not - .reverted; + if (alreadyPaused) { + await tx; + } else { + await expect(tx) + .to.emit( + pauseManager, + pauseManager.interface.events['ContractPauseStatusChange(address,bool)'] + .name, + ) + .withArgs(contract.address, true); + } - expect(await vault.paused()).eq(false); + expect(await pauseManager.contractPaused(contract.address)).eq(true); + expect(await pauseManager.isPaused(contract.address, '0x00000000')).eq(true); +}; + +export const adminUnpauseContractTest = async ( + { pauseManager, owner }: PauseParams, + contract: Contract, + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + if ( + await handleRevert( + pauseManager + .connect(from) + .contractAdminUnpause.bind(this, contract.address), + pauseManager, + opt, + ) + ) { + return; + } + const alreadyPaused = await pauseManager.contractPaused(contract.address); + const tx = pauseManager.connect(from).contractAdminUnpause(contract.address); + + if (!alreadyPaused) { + await tx; + } else { + await expect(tx) + .to.emit( + pauseManager, + pauseManager.interface.events['ContractPauseStatusChange(address,bool)'] + .name, + ) + .withArgs(contract.address, false); + } + + expect(await pauseManager.contractPaused(contract.address)).eq(false); + expect(await pauseManager.isPaused(contract.address, '0x00000000')).eq(false); }; export const mintToken = async ( - token: ERC20Mock | MTBILL, + token: ERC20Mock | MToken | USTBMock, to: AccountOrContract, amountN: number, ) => { @@ -182,12 +576,126 @@ export const tokenAmountFromBase18 = async ( }; export const balanceOfBase18 = async ( - token: ERC20 | IERC20Metadata, + token: ERC20 | IERC20Metadata | string, of: AccountOrContract, ) => { + if (typeof token === 'string') { + token = ERC20__factory.connect(token, ethers.provider); + } + if (token.address === ethers.constants.AddressZero) return ethers.constants.Zero; of = getAccount(of); const balance = await token.balanceOf(of); return tokenAmountToBase18(token, balance); }; + +export const getCurrentBlockTimestamp = async () => { + return (await ethers.provider.getBlock('latest')).timestamp; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Constructor = new (...args: any[]) => T; + +export const validateImplementation = async ( + _implementationFactory: Constructor | ContractFactory, +) => { + // FIXME: hardhat-upgrades call fails because it does not accept the constructor arguments + // const factory = + // typeof implementationFactory === 'function' + // ? new implementationFactory() + // : implementationFactory; + // await hre.upgrades.validateImplementation(factory); +}; + +export type InitializeParamCase = { + title: string; + params: + | Partial + | (( + fixture: DefaultFixture, + contract?: Contract, + ) => Partial | Promise>); + contract?: + | Contract + | ((fixture: DefaultFixture) => Contract | Promise); + revertCustomError: { + customErrorName: string; + args?: + | unknown[] + | ((fixture: DefaultFixture, contract?: Contract) => unknown[]); + }; +}; + +export type InitializeParamsOpt = OptionalCommonParams & { + contract?: Contract; +}; + +const runInitializeParamCase = async ( + fixture: DefaultFixture, + paramCase: InitializeParamCase, + initializeFunction: ( + fixture: DefaultFixture, + params: Partial, + opt?: InitializeParamsOpt, + ) => Promise, +) => { + const contract = + paramCase.contract === undefined + ? undefined + : typeof paramCase.contract === 'function' + ? await paramCase.contract(fixture) + : paramCase.contract; + + const params = + typeof paramCase.params === 'function' + ? await paramCase.params(fixture, contract) + : paramCase.params; + + const args = + typeof paramCase.revertCustomError.args === 'function' + ? paramCase.revertCustomError.args(fixture, contract) + : paramCase.revertCustomError.args; + + await initializeFunction(fixture, params, { + contract, + revertCustomError: { + customErrorName: paramCase.revertCustomError.customErrorName, + args, + }, + }); +}; + +export const initializeParamsSuits = ( + paramCases: InitializeParamCase[], + fixtureFn: () => Promise, + initializeFunction: ( + fixture: DefaultFixture, + params: Partial, + opt?: InitializeParamsOpt, + ) => Promise, +) => { + describe('initialization params', () => { + // TODO: should replace with async? + for (const paramCase of paramCases) { + it(`should fail: when ${paramCase.title}`, async () => { + const fixture = await fixtureFn(); + await runInitializeParamCase(fixture, paramCase, initializeFunction); + }); + } + }); +}; + +export const asyncForEach = async ( + array: Iterable, + callback: (item: T) => Promise, + sync = false, +) => { + if (sync) { + for (const item of array) { + await callback(item); + } + } else { + return await Promise.all(Array.from(array).map(callback)); + } +}; diff --git a/test/common/custom-feed-growth.helpers.ts b/test/common/custom-feed-growth.helpers.ts index 9ad7bf4e..6c13c1f6 100644 --- a/test/common/custom-feed-growth.helpers.ts +++ b/test/common/custom-feed-growth.helpers.ts @@ -3,7 +3,11 @@ import { expect } from 'chai'; import { BigNumber, BigNumberish } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; -import { OptionalCommonParams } from './common.helpers'; +import { + handleRevert, + OptionalCommonParams, + shouldRevert, +} from './common.helpers'; import { defaultDeploy } from './fixtures'; type CommonParamsSetRoundData = Pick< @@ -18,10 +22,13 @@ export const setOnlyUp = async ( ) => { const sender = opt?.from ?? owner; - if (opt?.revertMessage) { - await expect( - customFeedGrowth.connect(sender).setOnlyUp(newOnlyUp), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + customFeedGrowth.connect(sender).setOnlyUp.bind(this, newOnlyUp), + customFeedGrowth, + opt, + ) + ) { return; } @@ -48,10 +55,15 @@ export const setMinGrowthApr = async ( 8, ); - if (opt?.revertMessage) { - await expect( - customFeedGrowth.connect(sender).setMinGrowthApr(newMinGrowthAprParsed), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + customFeedGrowth + .connect(sender) + .setMinGrowthApr.bind(this, newMinGrowthAprParsed), + customFeedGrowth, + opt, + ) + ) { return; } @@ -80,10 +92,15 @@ export const setMaxGrowthApr = async ( 8, ); - if (opt?.revertMessage) { - await expect( - customFeedGrowth.connect(sender).setMaxGrowthApr(newMaxGrowthAprParsed), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + customFeedGrowth + .connect(sender) + .setMaxGrowthApr.bind(this, newMaxGrowthAprParsed), + customFeedGrowth, + opt, + ) + ) { return; } @@ -142,8 +159,7 @@ export const setRoundDataGrowth = async ( .connect(sender) .setRoundData.bind(this, dataParsed, timestamp, growthParsed); - if (opt?.revertMessage) { - await expect(callFn()).revertedWith(opt?.revertMessage); + if (await handleRevert(callFn, customFeedGrowth, opt)) { return; } @@ -244,7 +260,7 @@ export const setRoundDataSafeGrowth = async ( growthApr: number, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { + if (shouldRevert(opt)) { await setRoundDataGrowth( { customFeedGrowth, @@ -299,5 +315,38 @@ export const applyGrowth = ({ return BigNumber.from(answer).add(interest); }; +export const setMaxAnswerDeviationTest = async ( + { customFeedGrowth, owner }: CommonParamsSetRoundData, + maxAnswerDeviation: BigNumberish, + opt?: OptionalCommonParams, +) => { + const sender = opt?.from ?? owner; + + if ( + await handleRevert( + customFeedGrowth + .connect(sender) + .setMaxAnswerDeviation.bind(this, maxAnswerDeviation), + customFeedGrowth, + opt, + ) + ) { + return; + } + + await expect( + customFeedGrowth.connect(sender).setMaxAnswerDeviation(maxAnswerDeviation), + ) + .to.emit( + customFeedGrowth, + customFeedGrowth.interface.events['MaxAnswerDeviationUpdated(uint256)'] + .name, + ) + .withArgs(sender, maxAnswerDeviation).to.not.reverted; + + const maxAnswerDeviationAfter = await customFeedGrowth.maxAnswerDeviation(); + expect(maxAnswerDeviationAfter).eq(maxAnswerDeviation); +}; + export const calculatePriceDiviation = (last: number, next: number) => Math.abs(((next - last) * 100) / last); diff --git a/test/common/custom-feed.helpers.ts b/test/common/custom-feed.helpers.ts index c176ea06..ce78557a 100644 --- a/test/common/custom-feed.helpers.ts +++ b/test/common/custom-feed.helpers.ts @@ -1,8 +1,9 @@ import { setNextBlockTimestamp } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time'; import { expect } from 'chai'; +import { BigNumberish } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; -import { OptionalCommonParams } from './common.helpers'; +import { handleRevert, OptionalCommonParams } from './common.helpers'; import { defaultDeploy } from './fixtures'; type CommonParamsSetRoundData = Pick< @@ -19,10 +20,13 @@ export const setRoundData = async ( const dataParsed = parseUnits(data.toFixed(8).replace(/\.?0+$/, ''), 8); - if (opt?.revertMessage) { - await expect( - customFeed.connect(sender).setRoundData(dataParsed), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + customFeed.connect(sender).setRoundData.bind(this, dataParsed), + customFeed, + opt, + ) + ) { return; } @@ -74,10 +78,13 @@ export const setRoundDataSafe = async ( const dataParsed = parseUnits(data.toFixed(8).replace(/\.?0+$/, ''), 8); - if (opt?.revertMessage) { - await expect( - customFeed.connect(sender).setRoundDataSafe(dataParsed), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + customFeed.connect(sender).setRoundDataSafe.bind(this, dataParsed), + customFeed, + opt, + ) + ) { return; } @@ -120,5 +127,37 @@ export const setRoundDataSafe = async ( expect(await customFeed.lastAnswer()).eq(lastRoundDataAfter.answer); }; +export const setMaxAnswerDeviationTest = async ( + { customFeed, owner }: CommonParamsSetRoundData, + maxAnswerDeviation: BigNumberish, + opt?: OptionalCommonParams, +) => { + const sender = opt?.from ?? owner; + + if ( + await handleRevert( + customFeed + .connect(sender) + .setMaxAnswerDeviation.bind(this, maxAnswerDeviation), + customFeed, + opt, + ) + ) { + return; + } + + await expect( + customFeed.connect(sender).setMaxAnswerDeviation(maxAnswerDeviation), + ) + .to.emit( + customFeed, + customFeed.interface.events['MaxAnswerDeviationUpdated(uint256)'].name, + ) + .withArgs(maxAnswerDeviation).to.not.reverted; + + const maxAnswerDeviationAfter = await customFeed.maxAnswerDeviation(); + expect(maxAnswerDeviationAfter).eq(maxAnswerDeviation); +}; + export const calculatePriceDiviation = (last: number, next: number) => Math.abs(((next - last) * 100) / last); diff --git a/test/common/data-feed.helpers.ts b/test/common/data-feed.helpers.ts index b13b75b9..7fd1d538 100644 --- a/test/common/data-feed.helpers.ts +++ b/test/common/data-feed.helpers.ts @@ -1,6 +1,13 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { BigNumberish } from 'ethers'; import { formatUnits, parseUnits } from 'ethers/lib/utils'; -import { amountToBase18 } from './common.helpers'; +import { + amountToBase18, + handleRevert, + OptionalCommonParams, +} from './common.helpers'; import { defaultDeploy } from './fixtures'; type CommonParams = Pick< @@ -8,6 +15,34 @@ type CommonParams = Pick< 'mockedAggregator' >; +type CommonParamsSetHealthyDiff = Pick< + Awaited>, + 'dataFeed' | 'owner' +>; + +export const setHealthyDiffTest = async ( + { dataFeed, owner }: CommonParamsSetHealthyDiff, + healthyDiff: BigNumberish, + opt?: OptionalCommonParams, +) => { + const sender: SignerWithAddress = opt?.from ?? owner; + + if ( + await handleRevert( + () => dataFeed.connect(sender).setHealthyDiff(healthyDiff), + dataFeed, + opt, + ) + ) { + return; + } + + await expect(dataFeed.connect(sender).setHealthyDiff(healthyDiff)).not + .reverted; + + expect(await dataFeed.healthyDiff()).eq(healthyDiff); +}; + export const setRoundData = async ( { mockedAggregator }: CommonParams, newPrice: number, diff --git a/test/common/deploy.helpers.ts b/test/common/deploy.helpers.ts index f6a7bb04..87563a16 100644 --- a/test/common/deploy.helpers.ts +++ b/test/common/deploy.helpers.ts @@ -11,7 +11,7 @@ export const deployProxyContract = async < TContract extends Contract = Contract, >( contractName: string, - initParams?: unknown[], + initParams?: unknown[] | readonly unknown[], initializer = 'initialize', constructorParams?: unknown[], ): Promise => { diff --git a/test/common/deposit-vault-aave.helpers.ts b/test/common/deposit-vault-aave.helpers.ts index 4ca9402c..f5385c63 100644 --- a/test/common/deposit-vault-aave.helpers.ts +++ b/test/common/deposit-vault-aave.helpers.ts @@ -6,6 +6,8 @@ import { OptionalCommonParams, balanceOfBase18, getAccount, + handleRevert, + shouldRevert, } from './common.helpers'; import { depositInstantTest, @@ -42,12 +44,15 @@ export const setAaveDepositsEnabledTest = async ( enabled: boolean, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( depositVaultWithAave .connect(opt?.from ?? owner) - .setAaveDepositsEnabled(enabled), - ).revertedWith(opt?.revertMessage); + .setAaveDepositsEnabled.bind(this, enabled), + depositVaultWithAave, + opt, + ) + ) { return; } @@ -70,10 +75,15 @@ export const setAavePoolTest = async ( pool: string, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - depositVaultWithAave.connect(opt?.from ?? owner).setAavePool(token, pool), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + depositVaultWithAave + .connect(opt?.from ?? owner) + .setAavePool.bind(this, token, pool), + depositVaultWithAave, + opt, + ) + ) { return; } @@ -81,9 +91,7 @@ export const setAavePoolTest = async ( depositVaultWithAave.connect(opt?.from ?? owner).setAavePool(token, pool), ).to.emit( depositVaultWithAave, - depositVaultWithAave.interface.events[ - 'SetAavePool(address,address,address)' - ].name, + depositVaultWithAave.interface.events['SetAavePool(address,address)'].name, ).to.not.reverted; const poolAfter = await depositVaultWithAave.aavePools(token); @@ -95,10 +103,15 @@ export const removeAavePoolTest = async ( token: string, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - depositVaultWithAave.connect(opt?.from ?? owner).removeAavePool(token), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + depositVaultWithAave + .connect(opt?.from ?? owner) + .removeAavePool.bind(this, token), + depositVaultWithAave, + opt, + ) + ) { return; } @@ -106,8 +119,7 @@ export const removeAavePoolTest = async ( depositVaultWithAave.connect(opt?.from ?? owner).removeAavePool(token), ).to.emit( depositVaultWithAave, - depositVaultWithAave.interface.events['RemoveAavePool(address,address)'] - .name, + depositVaultWithAave.interface.events['RemoveAavePool(address)'].name, ).to.not.reverted; const poolAfter = await depositVaultWithAave.aavePools(token); @@ -137,7 +149,7 @@ export const depositInstantWithAaveTest = async ( ) => { tokenIn = getAccount(tokenIn); - if (opt?.revertMessage) { + if (shouldRevert(opt)) { await depositInstantTest( { depositVault: depositVaultWithAave, @@ -203,12 +215,15 @@ export const setAutoInvestFallbackEnabledAaveTest = async ( enabled: boolean, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( depositVaultWithAave .connect(opt?.from ?? owner) - .setAutoInvestFallbackEnabled(enabled), - ).revertedWith(opt?.revertMessage); + .setAutoInvestFallbackEnabled.bind(this, enabled), + depositVaultWithAave, + opt, + ) + ) { return; } @@ -248,7 +263,7 @@ export const depositRequestWithAaveTest = async ( ) => { tokenIn = getAccount(tokenIn); - if (opt?.revertMessage) { + if (shouldRevert(opt)) { await depositRequestTest( { depositVault: depositVaultWithAave, diff --git a/test/common/deposit-vault-morpho.helpers.ts b/test/common/deposit-vault-morpho.helpers.ts index c6c4c8ca..15fd5a16 100644 --- a/test/common/deposit-vault-morpho.helpers.ts +++ b/test/common/deposit-vault-morpho.helpers.ts @@ -6,6 +6,8 @@ import { OptionalCommonParams, balanceOfBase18, getAccount, + handleRevert, + shouldRevert, } from './common.helpers'; import { depositInstantTest, @@ -42,12 +44,15 @@ export const setMorphoDepositsEnabledTest = async ( enabled: boolean, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( depositVaultWithMorpho .connect(opt?.from ?? owner) - .setMorphoDepositsEnabled(enabled), - ).revertedWith(opt?.revertMessage); + .setMorphoDepositsEnabled.bind(this, enabled), + depositVaultWithMorpho, + opt, + ) + ) { return; } @@ -72,12 +77,15 @@ export const setMorphoVaultTest = async ( vault: string, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( depositVaultWithMorpho .connect(opt?.from ?? owner) - .setMorphoVault(token, vault), - ).revertedWith(opt?.revertMessage); + .setMorphoVault.bind(this, token, vault), + depositVaultWithMorpho, + opt, + ) + ) { return; } @@ -87,9 +95,8 @@ export const setMorphoVaultTest = async ( .setMorphoVault(token, vault), ).to.emit( depositVaultWithMorpho, - depositVaultWithMorpho.interface.events[ - 'SetMorphoVault(address,address,address)' - ].name, + depositVaultWithMorpho.interface.events['SetMorphoVault(address,address)'] + .name, ).to.not.reverted; const vaultAfter = await depositVaultWithMorpho.morphoVaults(token); @@ -101,12 +108,15 @@ export const removeMorphoVaultTest = async ( token: string, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( depositVaultWithMorpho .connect(opt?.from ?? owner) - .removeMorphoVault(token), - ).revertedWith(opt?.revertMessage); + .removeMorphoVault.bind(this, token), + depositVaultWithMorpho, + opt, + ) + ) { return; } @@ -114,9 +124,7 @@ export const removeMorphoVaultTest = async ( depositVaultWithMorpho.connect(opt?.from ?? owner).removeMorphoVault(token), ).to.emit( depositVaultWithMorpho, - depositVaultWithMorpho.interface.events[ - 'RemoveMorphoVault(address,address)' - ].name, + depositVaultWithMorpho.interface.events['RemoveMorphoVault(address)'].name, ).to.not.reverted; const vaultAfter = await depositVaultWithMorpho.morphoVaults(token); @@ -146,7 +154,7 @@ export const depositInstantWithMorphoTest = async ( ) => { tokenIn = getAccount(tokenIn); - if (opt?.revertMessage) { + if (shouldRevert(opt)) { await depositInstantTest( { depositVault: depositVaultWithMorpho, @@ -211,12 +219,15 @@ export const setAutoInvestFallbackEnabledMorphoTest = async ( enabled: boolean, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( depositVaultWithMorpho .connect(opt?.from ?? owner) - .setAutoInvestFallbackEnabled(enabled), - ).revertedWith(opt?.revertMessage); + .setAutoInvestFallbackEnabled.bind(this, enabled), + depositVaultWithMorpho, + opt, + ) + ) { return; } @@ -257,7 +268,7 @@ export const depositRequestWithMorphoTest = async ( ) => { tokenIn = getAccount(tokenIn); - if (opt?.revertMessage) { + if (shouldRevert(opt)) { await depositRequestTest( { depositVault: depositVaultWithMorpho, diff --git a/test/common/deposit-vault-mtoken.helpers.ts b/test/common/deposit-vault-mtoken.helpers.ts index 78667460..3c1d0fc3 100644 --- a/test/common/deposit-vault-mtoken.helpers.ts +++ b/test/common/deposit-vault-mtoken.helpers.ts @@ -6,6 +6,8 @@ import { OptionalCommonParams, balanceOfBase18, getAccount, + handleRevert, + shouldRevert, } from './common.helpers'; import { depositInstantTest, @@ -40,12 +42,15 @@ export const setMTokenDepositsEnabledTest = async ( enabled: boolean, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( depositVaultWithMToken .connect(opt?.from ?? owner) - .setMTokenDepositsEnabled(enabled), - ).revertedWith(opt?.revertMessage); + .setMTokenDepositsEnabled.bind(this, enabled), + depositVaultWithMToken, + opt, + ) + ) { return; } @@ -69,12 +74,15 @@ export const setMTokenDepositVaultTest = async ( newVault: string, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( depositVaultWithMToken .connect(opt?.from ?? owner) - .setMTokenDepositVault(newVault), - ).revertedWith(opt?.revertMessage); + .setMTokenDepositVault.bind(this, newVault), + depositVaultWithMToken, + opt, + ) + ) { return; } @@ -84,9 +92,8 @@ export const setMTokenDepositVaultTest = async ( .setMTokenDepositVault(newVault), ).to.emit( depositVaultWithMToken, - depositVaultWithMToken.interface.events[ - 'SetMTokenDepositVault(address,address)' - ].name, + depositVaultWithMToken.interface.events['SetMTokenDepositVault(address)'] + .name, ).to.not.reverted; const vaultAfter = await depositVaultWithMToken.mTokenDepositVault(); @@ -115,7 +122,7 @@ export const depositInstantWithMTokenTest = async ( ) => { tokenIn = getAccount(tokenIn); - if (opt?.revertMessage) { + if (shouldRevert(opt)) { await depositInstantTest( { depositVault: depositVaultWithMToken, @@ -125,7 +132,7 @@ export const depositInstantWithMTokenTest = async ( waivedFee, minAmount, customRecipient, - checkTokensReceiver: !expectedMTokenDeposited, + checkTokensReceiver: true, }, tokenIn, amountUsdIn, @@ -161,7 +168,7 @@ export const depositInstantWithMTokenTest = async ( waivedFee, minAmount, customRecipient, - checkTokensReceiver: !expectedMTokenDeposited, + checkTokensReceiver: true, }, tokenIn, amountUsdIn, @@ -189,12 +196,15 @@ export const setAutoInvestFallbackEnabledMTokenTest = async ( enabled: boolean, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( depositVaultWithMToken .connect(opt?.from ?? owner) - .setAutoInvestFallbackEnabled(enabled), - ).revertedWith(opt?.revertMessage); + .setAutoInvestFallbackEnabled.bind(this, enabled), + depositVaultWithMToken, + opt, + ) + ) { return; } @@ -234,7 +244,7 @@ export const depositRequestWithMTokenTest = async ( ) => { tokenIn = getAccount(tokenIn); - if (opt?.revertMessage) { + if (shouldRevert(opt)) { await depositRequestTest( { depositVault: depositVaultWithMToken, @@ -243,7 +253,8 @@ export const depositRequestWithMTokenTest = async ( mTokenToUsdDataFeed, waivedFee, customRecipient, - checkTokensReceiver: !expectedMTokenDeposited, + checkTokensReceiver: true, + checkMTokenSupplyUnchanged: !expectedMTokenDeposited, }, tokenIn, amountUsdIn, @@ -277,7 +288,8 @@ export const depositRequestWithMTokenTest = async ( mTokenToUsdDataFeed, waivedFee, customRecipient, - checkTokensReceiver: !expectedMTokenDeposited, + checkTokensReceiver: true, + checkMTokenSupplyUnchanged: !expectedMTokenDeposited, }, tokenIn, amountUsdIn, diff --git a/test/common/deposit-vault-ustb.helpers.ts b/test/common/deposit-vault-ustb.helpers.ts index e3916300..7f18a756 100644 --- a/test/common/deposit-vault-ustb.helpers.ts +++ b/test/common/deposit-vault-ustb.helpers.ts @@ -5,6 +5,8 @@ import { AccountOrContract, OptionalCommonParams, getAccount, + handleRevert, + shouldRevert, } from './common.helpers'; import { depositInstantTest } from './deposit-vault.helpers'; import { defaultDeploy } from './fixtures'; @@ -57,12 +59,15 @@ export const setUstbDepositsEnabledTest = async ( enabled: boolean, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( depositVaultWithUSTB .connect(opt?.from ?? owner) - .setUstbDepositsEnabled(enabled), - ).revertedWith(opt?.revertMessage); + .setUstbDepositsEnabled.bind(this, enabled), + depositVaultWithUSTB, + opt, + ) + ) { return; } @@ -104,7 +109,7 @@ export const depositInstantWithUstbTest = async ( ) => { tokenIn = getAccount(tokenIn); - if (opt?.revertMessage) { + if (shouldRevert(opt)) { await depositInstantTest( { depositVault: depositVaultWithUSTB, diff --git a/test/common/deposit-vault.helpers.ts b/test/common/deposit-vault.helpers.ts index 26ea8a44..47e33c69 100644 --- a/test/common/deposit-vault.helpers.ts +++ b/test/common/deposit-vault.helpers.ts @@ -1,15 +1,24 @@ +import { setNextBlockTimestamp } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { BigNumber, BigNumberish, constants } from 'ethers'; -import { parseUnits } from 'ethers/lib/utils'; +import { + BigNumber, + BigNumberish, + constants, + ContractTransaction, +} from 'ethers'; +import { formatUnits, parseUnits } from 'ethers/lib/utils'; import { AccountOrContract, OptionalCommonParams, balanceOfBase18, getAccount, + getCurrentBlockTimestamp, + handleRevert, } from './common.helpers'; import { defaultDeploy } from './fixtures'; +import { calculateWindowRateLimitCapacity } from './manageable-vault.helpers'; import { DataFeedTest__factory, @@ -24,37 +33,59 @@ import { MToken, } from '../../typechain-types'; +type DepositVaultType = + | DepositVault + | DepositVaultTest + | DepositVaultWithAaveTest + | DepositVaultWithMorphoTest + | DepositVaultWithMTokenTest + | DepositVaultWithUSTBTest; type CommonParamsDeposit = { - depositVault: - | DepositVault - | DepositVaultTest - | DepositVaultWithAaveTest - | DepositVaultWithMorphoTest - | DepositVaultWithMTokenTest - | DepositVaultWithUSTBTest; + depositVault: DepositVaultType; mTBILL: MToken; } & Pick< Awaited>, 'owner' | 'mTokenToUsdDataFeed' >; +const getTotalFromInstantShare = ( + amountIn: BigNumber, + instantShare?: BigNumberish, +) => { + if (instantShare === undefined) { + return amountIn; + } + + if (BigNumber.from(instantShare).eq(constants.Zero)) { + return BigNumber.from(0); + } + + return amountIn.mul(100_00).div(instantShare); +}; + export const depositInstantTest = async ( { depositVault, owner, mTBILL, mTokenToUsdDataFeed, - waivedFee, minAmount, customRecipient, checkTokensReceiver = true, expectedMintAmount, + referrerId, + holdback, }: CommonParamsDeposit & { waivedFee?: boolean; minAmount?: BigNumberish; customRecipient?: AccountOrContract; checkTokensReceiver?: boolean; expectedMintAmount?: BigNumberish; + referrerId?: string; + holdback?: { + callFunction: () => Promise; + instantShare: BigNumberish; + }; }, tokenIn: ERC20 | string, amountUsdIn: number, @@ -67,7 +98,6 @@ export const depositInstantTest = async ( const tokenContract = ERC20__factory.connect(tokenIn, owner); const tokensReceiver = await depositVault.tokensReceiver(); - const feeReceiver = await depositVault.feeReceiver(); const amountIn = parseUnits(amountUsdIn.toFixed(18).replace(/\.?0+$/, '')); @@ -76,40 +106,40 @@ export const depositInstantTest = async ( ? getAccount(customRecipient) : sender.address; - const callFn = withRecipient - ? depositVault - .connect(sender) - ['depositInstant(address,uint256,uint256,bytes32,address)'].bind( - this, - tokenIn, - amountIn, - minAmount ?? constants.Zero, - constants.HashZero, - recipient, - ) - : depositVault - .connect(sender) - ['depositInstant(address,uint256,uint256,bytes32)'].bind( - this, - tokenIn, - amountIn, - minAmount ?? constants.Zero, - constants.HashZero, - ); - - if (opt?.revertMessage) { - await expect(callFn()).revertedWith(opt?.revertMessage); + referrerId = referrerId ?? constants.HashZero; + + const callFn = + holdback?.callFunction ?? + (withRecipient + ? depositVault + .connect(sender) + ['depositInstant(address,uint256,uint256,bytes32,address)'].bind( + this, + tokenIn, + amountIn, + minAmount ?? constants.Zero, + referrerId, + recipient, + ) + : depositVault + .connect(sender) + ['depositInstant(address,uint256,uint256,bytes32)'].bind( + this, + tokenIn, + amountIn, + minAmount ?? constants.Zero, + referrerId, + )); + + if (await handleRevert(callFn, depositVault, opt)) { return; } - const balanceBeforeContract = await balanceOfBase18( + const balanceBeforeReceiver = await balanceOfBase18( tokenContract, tokensReceiver, ); - const feeReceiverBalanceBeforeContract = await balanceOfBase18( - tokenContract, - feeReceiver, - ); + const balanceBeforeUser = await balanceOfBase18( tokenContract, sender.address, @@ -122,58 +152,78 @@ export const depositInstantTest = async ( const mTokenRate = await mTokenToUsdDataFeed.getDataInBase18(); - const { fee, mintAmount, amountInWithoutFee, actualAmountInUsd } = - await calcExpectedMintAmount( - sender, - tokenIn, - depositVault, - mTokenRate, - amountIn, - true, - ); + const { fee, mintAmount, tokenInRate } = await calcExpectedMintAmount( + sender, + tokenIn, + depositVault, + mTokenRate, + amountIn, + true, + ); - await expect(callFn()) + const instantLimitsBefore = await depositVault.getInstantLimitStatuses(); + const timestampBefore = await getCurrentBlockTimestamp(); + + const callPromise = callFn(); + + await expect(callPromise) .to.emit( depositVault, depositVault.interface.events[ - withRecipient - ? 'DepositInstantWithCustomRecipient(address,address,address,uint256,uint256,uint256,uint256,bytes32)' - : 'DepositInstant(address,address,uint256,uint256,uint256,uint256,bytes32)' + 'DepositInstant(address,address,address,uint256,uint256,uint256,uint256,uint256,bytes32)' ].name, ) .withArgs( - ...[ - sender.address, - tokenContract.address, - withRecipient ? recipient : undefined, - actualAmountInUsd, - amountUsdIn, - fee, - 0, - constants.HashZero, - ].filter((v) => v !== undefined), + sender.address, + tokenContract.address, + recipient, + fee, + mintAmount, + mTokenRate, + tokenInRate, + referrerId, ).to.not.reverted; + const instantLimitsAfter = await depositVault.getInstantLimitStatuses(); + const timestampAfter = await getCurrentBlockTimestamp(); + + const expectedMinted = expectedMintAmount ?? mintAmount; + + const expectedLimitsAfter = await Promise.all( + instantLimitsBefore.map(async (limit) => { + const { remaining, inFlight } = calculateWindowRateLimitCapacity({ + amountInFlight: limit.inFlight, + lastUpdated: timestampBefore, + limit: limit.limit, + window: limit.window, + now: timestampAfter, + }); + + return { + ...limit, + remaining: remaining.gte(expectedMinted) + ? remaining.sub(expectedMinted) + : constants.Zero, + inFlight: inFlight.add(expectedMinted), + }; + }), + ); + const totalMintedAfter = await depositVault.totalMinted(sender.address); const totalMintedAfterRecipient = await depositVault.totalMinted(recipient); - const balanceAfterContract = await balanceOfBase18( + const balanceAfterReceiver = await balanceOfBase18( tokenContract, tokensReceiver, ); - const feeReceiverBalanceAfterContract = await balanceOfBase18( - tokenContract, - feeReceiver, - ); + const balanceAfterUser = await balanceOfBase18(tokenContract, sender.address); const balanceMtBillAfterUser = await balanceOfBase18(mTBILL, recipient); const maxSupplyCapAfter = await depositVault.maxSupplyCap(); - const expectedMinted = expectedMintAmount ?? mintAmount; - - expect(balanceMtBillAfterUser.sub(balanceMtBillBeforeUser)).eq( - expectedMinted, + expect(balanceMtBillAfterUser).eq( + balanceMtBillBeforeUser.add(expectedMinted), ); expect(totalMintedAfter).eq(totalMintedBefore.add(expectedMinted)); if (recipient !== sender.address) { @@ -181,21 +231,28 @@ export const depositInstantTest = async ( } if (checkTokensReceiver) { - expect(balanceAfterContract).eq( - balanceBeforeContract.add(amountInWithoutFee), - ); + expect(balanceAfterReceiver).eq(balanceBeforeReceiver.add(amountIn)); } - expect(feeReceiverBalanceAfterContract).eq( - feeReceiverBalanceBeforeContract.add(fee), - ); - if (waivedFee) { - expect(feeReceiverBalanceAfterContract).eq( - feeReceiverBalanceBeforeContract, - ); + if (!holdback) { + expect(balanceAfterUser).eq(balanceBeforeUser.sub(amountIn)); } - expect(balanceAfterUser).eq(balanceBeforeUser.sub(amountIn)); + expect(maxSupplyCapAfter).eq(maxSupplyCapBefore); + + for (const [index, limit] of instantLimitsBefore.entries()) { + expect(instantLimitsAfter[index].inFlight).eq( + expectedLimitsAfter[index].inFlight, + ); + expect(instantLimitsAfter[index].remaining).eq( + expectedLimitsAfter[index].remaining, + ); + expect(instantLimitsAfter[index].lastUpdated).eq(timestampAfter); + expect(instantLimitsAfter[index].window).eq(limit.window); + expect(instantLimitsAfter[index].limit).eq(limit.limit); + } + + return callPromise; }; export const depositRequestTest = async ( @@ -206,10 +263,21 @@ export const depositRequestTest = async ( waivedFee, customRecipient, checkTokensReceiver = true, + checkMTokenSupplyUnchanged = true, + customRecipientInstant, + instantShare, + minReceiveAmountInstantShare, + mTBILL, + referrerId, }: CommonParamsDeposit & { + referrerId?: string; waivedFee?: boolean; customRecipient?: AccountOrContract; checkTokensReceiver?: boolean; + checkMTokenSupplyUnchanged?: boolean; + customRecipientInstant?: AccountOrContract; + instantShare?: BigNumberish; + minReceiveAmountInstantShare?: BigNumberish; }, tokenIn: ERC20 | string, amountUsdIn: number, @@ -222,7 +290,6 @@ export const depositRequestTest = async ( const tokenContract = ERC20__factory.connect(tokenIn, owner); const tokensReceiver = await depositVault.tokensReceiver(); - const feeReceiver = await depositVault.feeReceiver(); const amountIn = parseUnits(amountUsdIn.toFixed(18).replace(/\.?0+$/, '')); @@ -231,27 +298,39 @@ export const depositRequestTest = async ( ? getAccount(customRecipient) : sender.address; - const callFn = withRecipient - ? depositVault - .connect(sender) - ['depositRequest(address,uint256,bytes32,address)'].bind( - this, - tokenIn, - amountIn, - constants.HashZero, - recipient, - ) - : depositVault - .connect(sender) - ['depositRequest(address,uint256,bytes32)'].bind( - this, - tokenIn, - amountIn, - constants.HashZero, - ); + const recipientInstant = + customRecipientInstant || customRecipient + ? getAccount(customRecipientInstant ?? customRecipient!) + : sender.address; - if (opt?.revertMessage) { - await expect(callFn()).revertedWith(opt?.revertMessage); + referrerId = referrerId ?? constants.HashZero; + + const callFn = + instantShare || withRecipient + ? depositVault + .connect(sender) + [ + 'depositRequest(address,uint256,bytes32,address,uint256,uint256,address)' + ].bind( + this, + tokenIn, + amountIn, + referrerId, + recipient, + instantShare ?? constants.Zero, + minReceiveAmountInstantShare ?? constants.Zero, + recipientInstant, + ) + : depositVault + .connect(sender) + ['depositRequest(address,uint256,bytes32)'].bind( + this, + tokenIn, + amountIn, + referrerId, + ); + + if (await handleRevert(callFn, depositVault, opt)) { return {}; } @@ -259,10 +338,7 @@ export const depositRequestTest = async ( tokenContract, tokensReceiver, ); - const feeReceiverBalanceBeforeContract = await balanceOfBase18( - tokenContract, - feeReceiver, - ); + const balanceBeforeUser = await balanceOfBase18( tokenContract, sender.address, @@ -272,77 +348,137 @@ export const depositRequestTest = async ( const mTokenRate = await mTokenToUsdDataFeed.getDataInBase18(); const maxSupplyCapBefore = await depositVault.maxSupplyCap(); + const supplyBefore = await mTBILL.totalSupply(); + const upcomingSupplyBefore = await depositVault.upcomingSupply(); + + const amountTokenInInstant = amountIn + .mul(instantShare ?? constants.Zero) + .div(100_00); + const amountTokenInRequest = amountIn.sub(amountTokenInInstant); - const { fee, mintAmount, amountInWithoutFee, actualAmountInUsd } = - await calcExpectedMintAmount( - sender, + const calcMintAmountRequest = await calcExpectedMintAmount( + sender, + tokenIn, + depositVault, + mTokenRate, + amountTokenInRequest, + false, + ); + + const calcMintAmountInstant = await calcExpectedMintAmount( + sender, + tokenIn, + depositVault, + mTokenRate, + amountTokenInInstant, + true, + ); + + const nextExpectedRequestIdToProcessBefore = + await depositVault.nextExpectedRequestIdToProcess(); + + let callPromise: Awaited>; + + if (amountTokenInInstant.gt(0)) { + callPromise = await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + waivedFee, + minAmount: minReceiveAmountInstantShare ?? constants.Zero, + customRecipient: recipientInstant, + checkTokensReceiver: false, + holdback: { + callFunction: callFn, + instantShare: instantShare ?? constants.Zero, + }, + }, tokenIn, - depositVault, - mTokenRate, - amountIn, - false, + +formatUnits(amountTokenInInstant, 18), + { from: sender }, ); + } - await expect(callFn()) + await expect(callPromise ?? callFn()) .to.emit( depositVault, depositVault.interface.events[ - withRecipient - ? 'DepositRequestWithCustomRecipient(uint256,address,address,address,uint256,uint256,uint256,uint256,bytes32)' - : 'DepositRequest(uint256,address,address,uint256,uint256,uint256,uint256,bytes32)' + 'DepositRequest(uint256,address,address,address,uint256,uint256,uint256,uint256,uint256,bytes32)' ].name, ) .withArgs( - ...[ - latestRequestIdBefore.add(1), - sender.address, - tokenContract.address, - withRecipient ? recipient : undefined, - amountIn, - actualAmountInUsd, - fee, - mintAmount, - constants.HashZero, - ].filter((v) => v !== undefined), + latestRequestIdBefore, + sender.address, + tokenContract.address, + recipient, + amountTokenInRequest, + amountTokenInInstant, + calcMintAmountRequest.fee, + mTokenRate, + calcMintAmountRequest.tokenInRate, + referrerId, ).to.not.reverted; + const nextExpectedRequestIdToProcessAfter = + await depositVault.nextExpectedRequestIdToProcess(); + + expect(nextExpectedRequestIdToProcessAfter).eq( + nextExpectedRequestIdToProcessBefore, + ); + const latestRequestIdAfter = await depositVault.currentRequestId(); + const supplyAfter = await mTBILL.totalSupply(); const balanceAfterContract = await balanceOfBase18( tokenContract, tokensReceiver, ); - const feeReceiverBalanceAfterContract = await balanceOfBase18( - tokenContract, - feeReceiver, - ); + const balanceAfterUser = await balanceOfBase18(tokenContract, sender.address); const request = await depositVault.mintRequests(latestRequestIdBefore); const maxSupplyCapAfter = await depositVault.maxSupplyCap(); + const upcomingSupplyAfter = await depositVault.upcomingSupply(); - expect(request.depositedUsdAmount).eq(actualAmountInUsd); + expect(request.depositedUsdAmount).eq( + calcMintAmountRequest.actualAmountInUsd, + ); expect(request.tokenOutRate).eq(mTokenRate); - expect(request.sender).eq(recipient); + expect(request.recipient).eq(recipient); expect(request.status).eq(0); expect(request.tokenIn).eq(tokenContract.address); expect(latestRequestIdAfter).eq(latestRequestIdBefore.add(1)); + if (checkTokensReceiver) { expect(balanceAfterContract).eq( - balanceBeforeContract.add(amountInWithoutFee), + balanceBeforeContract.add(amountTokenInRequest.add(amountTokenInInstant)), ); - } - expect(feeReceiverBalanceAfterContract).eq( - feeReceiverBalanceBeforeContract.add(fee), - ); - if (waivedFee) { - expect(feeReceiverBalanceAfterContract).eq( - feeReceiverBalanceBeforeContract, + } else { + expect(balanceAfterContract).eq( + balanceBeforeContract.add( + calcMintAmountRequest.fee.add(calcMintAmountInstant.fee), + ), ); } + expect(balanceAfterUser).eq(balanceBeforeUser.sub(amountIn)); expect(maxSupplyCapAfter).eq(maxSupplyCapBefore); + const estimatedMintAmountRequest = calcMintAmountRequest.usdForMintConvertion + .mul(constants.WeiPerEther) + .div(request.tokenOutRate); + + expect(upcomingSupplyAfter).eq( + upcomingSupplyBefore.add(estimatedMintAmountRequest), + ); + + // those checks is already made in redeemInstantTest + if (checkMTokenSupplyUnchanged && !amountTokenInInstant.gt(0)) { + expect(supplyAfter).eq(supplyBefore); + } + return { requestId: latestRequestIdBefore, rate: mTokenRate, @@ -350,135 +486,193 @@ export const depositRequestTest = async ( }; export const approveRequestTest = async ( - { depositVault, owner, mTBILL }: CommonParamsDeposit, + { + depositVault, + owner, + mTBILL, + isAvgRate = false, + }: CommonParamsDeposit & { + isAvgRate?: boolean; + }, requestId: BigNumberish, newRate: BigNumberish, opt?: OptionalCommonParams, ) => { const sender = opt?.from ?? owner; - if (opt?.revertMessage) { - await expect( - depositVault.connect(sender).approveRequest(requestId, newRate), - ).revertedWith(opt?.revertMessage); + const callFn = depositVault + .connect(sender) + .approveRequest.bind(this, requestId, newRate, isAvgRate); + + if (await handleRevert(callFn, depositVault, opt)) { return; } - const balanceMtBillBeforeUser = await balanceOfBase18(mTBILL, sender.address); - - const totalDepositedBefore = await depositVault.totalMinted(sender.address); const requestData = await depositVault.mintRequests(requestId); - const feePercent = await getFeePercent( - requestData.sender, - requestData.tokenIn, - depositVault, - false, - ); - - const expectedMintAmount = requestData.depositedUsdAmount - .sub(requestData.depositedUsdAmount.mul(feePercent).div(10000)) - .mul(constants.WeiPerEther) - .div(newRate); - - await expect(depositVault.connect(sender).approveRequest(requestId, newRate)) - .to.emit( - depositVault, - depositVault.interface.events['ApproveRequest(uint256,uint256)'].name, - ) - .withArgs(requestId, newRate).to.not.reverted; - - const requestDataAfter = await depositVault.mintRequests(requestId); - - const totalDepositedAfter = await depositVault.totalMinted(sender.address); - - const balanceMtBillAfterUser = await balanceOfBase18(mTBILL, sender.address); - - expect(balanceMtBillAfterUser.sub(balanceMtBillBeforeUser)).eq( - expectedMintAmount, - ); - expect(totalDepositedAfter).eq(totalDepositedBefore.add(expectedMintAmount)); - expect(requestDataAfter.sender).eq(requestData.sender); - expect(requestDataAfter.tokenIn).eq(requestData.tokenIn); - expect(requestDataAfter.tokenOutRate).eq(newRate); - expect(requestDataAfter.depositedUsdAmount).eq( - requestData.depositedUsdAmount, - ); - expect(requestDataAfter.status).eq(1); -}; - -export const safeApproveRequestTest = async ( - { depositVault, owner, mTBILL }: CommonParamsDeposit, - requestId: BigNumberish, - newRate: BigNumberish, - opt?: OptionalCommonParams, -) => { - const sender = opt?.from ?? owner; + let actualRate = !isAvgRate + ? BigNumber.from(newRate) + : BigNumber.from( + expectedDepositHoldbackPartRateFromAvg( + requestData.depositedUsdAmount, + requestData.depositedInstantUsdAmount, + requestData.tokenOutRate, + newRate, + ), + ); - if (opt?.revertMessage) { - await expect( - depositVault.connect(sender).safeApproveRequest(requestId, newRate), - ).revertedWith(opt?.revertMessage); - return; + if (actualRate.eq(0)) { + actualRate = BigNumber.from(newRate); } - const requestData = await depositVault.mintRequests(requestId); const balanceMtBillBeforeUser = await balanceOfBase18( mTBILL, - requestData.sender, + requestData.recipient, ); const totalDepositedBefore = await depositVault.totalMinted( - requestData.sender, + requestData.recipient, ); + const balanceBeforeVaultMToken = await balanceOfBase18( + mTBILL, + depositVault.address, + ); + const totalSupplyBefore = await mTBILL.totalSupply(); + const feePercent = await getFeePercent( - requestData.sender, + requestData.recipient, requestData.tokenIn, depositVault, false, ); - const expectedMintAmount = requestData.depositedUsdAmount - .sub(requestData.depositedUsdAmount.mul(feePercent).div(10000)) + const expectedMintAmount = requestData.usdAmountWithoutFees .mul(constants.WeiPerEther) - .div(newRate); + .div(BigNumber.from(0).eq(actualRate) ? parseUnits('1') : actualRate); - await expect( - depositVault.connect(sender).safeApproveRequest(requestId, newRate), - ) + const nextExpectedRequestIdToProcessBefore = + await depositVault.nextExpectedRequestIdToProcess(); + + const upcomingSupplyBefore = await depositVault.upcomingSupply(); + await expect(callFn()) .to.emit( depositVault, - depositVault.interface.events['SafeApproveRequest(uint256,uint256)'].name, + depositVault.interface.events['ApproveRequest(uint256,uint256,bool,bool)'] + .name, ) - .withArgs(requestId, newRate).to.not.reverted; + .withArgs(requestId, actualRate, false, isAvgRate).to.not.reverted; + const nextExpectedRequestIdToProcessAfter = + await depositVault.nextExpectedRequestIdToProcess(); + + if (nextExpectedRequestIdToProcessBefore.lte(requestId)) { + expect(nextExpectedRequestIdToProcessAfter).eq( + BigNumber.from(requestId).add(1), + ); + } else { + expect(nextExpectedRequestIdToProcessAfter).eq( + nextExpectedRequestIdToProcessBefore, + ); + } + + const upcomingSupplyAfter = await depositVault.upcomingSupply(); const requestDataAfter = await depositVault.mintRequests(requestId); const totalDepositedAfter = await depositVault.totalMinted( - requestData.sender, + requestData.recipient, + ); + + const totalSupplyAfter = await mTBILL.totalSupply(); + + const balanceAfterVaultMToken = await balanceOfBase18( + mTBILL, + depositVault.address, ); + const estimatedMintAmountRequest = requestData.usdAmountWithoutFees + .mul(constants.WeiPerEther) + .div(requestData.tokenOutRate); + const balanceMtBillAfterUser = await balanceOfBase18( mTBILL, - requestData.sender, + requestData.recipient, ); + expect(upcomingSupplyAfter).eq( + upcomingSupplyBefore.sub(estimatedMintAmountRequest), + ); + + expect(totalSupplyAfter).eq(totalSupplyBefore.add(expectedMintAmount)); + expect(balanceMtBillAfterUser.sub(balanceMtBillBeforeUser)).eq( expectedMintAmount, ); + expect(balanceAfterVaultMToken).eq(balanceBeforeVaultMToken); + expect(requestDataAfter.status).eq(1); expect(totalDepositedAfter).eq(totalDepositedBefore.add(expectedMintAmount)); - expect(requestDataAfter.sender).eq(requestData.sender); + + expect(requestDataAfter.amountMToken).eq(expectedMintAmount); + expect(requestDataAfter.recipient).eq(requestData.recipient); expect(requestDataAfter.tokenIn).eq(requestData.tokenIn); - expect(requestDataAfter.tokenOutRate).eq(newRate); + expect(requestDataAfter.tokenOutRate).eq(requestData.tokenOutRate); + expect(requestDataAfter.approvedTokenOutRate).eq(actualRate); expect(requestDataAfter.depositedUsdAmount).eq( requestData.depositedUsdAmount, ); - expect(requestDataAfter.status).eq(1); + expect(requestDataAfter.depositedInstantUsdAmount).eq( + requestData.depositedInstantUsdAmount, + ); +}; + +export const expectedDepositHoldbackPartRateFromAvg = ( + depositedUsdAmount: BigNumberish, + depositedInstantUsdAmount: BigNumberish, + tokenOutRate: BigNumberish, + avgMTokenRate: BigNumberish, +): bigint => { + depositedUsdAmount = BigInt(depositedUsdAmount.toString()); + depositedInstantUsdAmount = BigInt(depositedInstantUsdAmount.toString()); + tokenOutRate = BigInt(tokenOutRate.toString()); + avgMTokenRate = BigInt(avgMTokenRate.toString()); + + if ( + avgMTokenRate === 0n || + tokenOutRate === 0n || + depositedInstantUsdAmount === 0n + ) { + return 0n; + } + + const targetTotalMTokenValue = + ((depositedUsdAmount + depositedInstantUsdAmount) * 10n ** 18n) / + avgMTokenRate; + const instantPartMTokenValue = + (depositedInstantUsdAmount * 10n ** 18n) / tokenOutRate; + + if (targetTotalMTokenValue <= instantPartMTokenValue) { + return 0n; + } + + const holdbackPartValue = targetTotalMTokenValue - instantPartMTokenValue; + + if (holdbackPartValue === 0n) { + return 0n; + } + + return (depositedUsdAmount * 10n ** 18n) / holdbackPartValue; }; export const safeBulkApproveRequestTest = async ( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }: CommonParamsDeposit, + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate = false, + }: CommonParamsDeposit & { + isAvgRate?: boolean; + }, requests: { id: BigNumberish; expectedToExecute?: boolean }[], newRate?: BigNumberish | 'request-rate', opt?: OptionalCommonParams, @@ -487,11 +681,11 @@ export const safeBulkApproveRequestTest = async ( const requestIds = requests.map(({ id }) => id); - const callFn = - newRate && newRate !== 'request-rate' + const callFn = isAvgRate + ? newRate && newRate !== 'request-rate' ? depositVault .connect(sender) - ['safeBulkApproveRequest(uint256[],uint256)'].bind( + ['safeBulkApproveRequestAvgRate(uint256[],uint256)'].bind( this, requestIds, newRate, @@ -502,29 +696,49 @@ export const safeBulkApproveRequestTest = async ( .safeBulkApproveRequestAtSavedRate.bind(this, requestIds) : depositVault .connect(sender) - ['safeBulkApproveRequest(uint256[])'].bind(this, requestIds); + ['safeBulkApproveRequestAvgRate(uint256[])'].bind(this, requestIds) + : newRate && newRate !== 'request-rate' + ? depositVault + .connect(sender) + ['safeBulkApproveRequest(uint256[],uint256)'].bind( + this, + requestIds, + newRate, + ) + : newRate === 'request-rate' + ? depositVault + .connect(sender) + .safeBulkApproveRequestAtSavedRate.bind(this, requestIds) + : depositVault + .connect(sender) + ['safeBulkApproveRequest(uint256[])'].bind(this, requestIds); - if (opt?.revertMessage) { - await expect(callFn()).revertedWith(opt?.revertMessage); + if (await handleRevert(callFn, depositVault, opt)) { return; } + await setNextBlockTimestamp((await getCurrentBlockTimestamp()) + 1); + const requestDatasBefore = await Promise.all( requestIds.map((requestId) => depositVault.mintRequests(requestId)), ); const balancesBefore = await Promise.all( - requestDatasBefore.map(({ sender }) => balanceOfBase18(mTBILL, sender)), + requestDatasBefore.map(({ recipient }) => + balanceOfBase18(mTBILL, recipient), + ), ); const totalDepositedsBefore = await Promise.all( - requestDatasBefore.map(({ sender }) => depositVault.totalMinted(sender)), + requestDatasBefore.map(({ recipient }) => + depositVault.totalMinted(recipient), + ), ); const feePercents = await Promise.all( requestDatasBefore.map((requestData) => getFeePercent( - requestData.sender, + requestData.recipient, requestData.tokenIn, depositVault, false, @@ -535,18 +749,73 @@ export const safeBulkApproveRequestTest = async ( const totalSupplyBefore = await mTBILL.totalSupply(); const supplyCap = await depositVault.maxSupplyCap(); + const upcomingSupplyBefore = await depositVault.upcomingSupply(); + + const nextExpectedRequestIdToProcessBefore = + await depositVault.nextExpectedRequestIdToProcess(); + const txPromise = callFn(); await expect(txPromise).to.not.reverted; + const nextExpectedRequestIdToProcessAfter = + await depositVault.nextExpectedRequestIdToProcess(); + + const expectedToExecuteRequests = requests.filter( + (v) => v.expectedToExecute ?? true, + ); + + const expectedHighestProcessedRequestId = expectedToExecuteRequests.sort( + (a, b) => +b.id.toString() - +a.id.toString(), + )[0]?.id; + + if ( + expectedHighestProcessedRequestId !== undefined && + nextExpectedRequestIdToProcessBefore.lte(expectedHighestProcessedRequestId) + ) { + expect(nextExpectedRequestIdToProcessAfter).eq( + BigNumber.from(expectedHighestProcessedRequestId).add(1), + ); + } else { + expect(nextExpectedRequestIdToProcessAfter).eq( + nextExpectedRequestIdToProcessBefore, + ); + } + + const upcomingSupplyAfter = await depositVault.upcomingSupply(); + const currentRate = await mTokenToUsdDataFeed.getDataInBase18(); - const newExpectedRate = - newRate === 'request-rate' ? undefined : newRate ?? currentRate; + + const newExpectedRate = ( + requestData: (typeof requestDatasBefore)[number], + ) => { + let rate = + newRate === 'request-rate' + ? requestData.tokenOutRate + : newRate ?? currentRate; + + if (isAvgRate) { + const holdbackRate = expectedDepositHoldbackPartRateFromAvg( + requestData.depositedUsdAmount, + requestData.depositedInstantUsdAmount, + requestData.tokenOutRate, + rate, + ); + rate = holdbackRate === 0n ? rate : holdbackRate; + } + return BigNumber.from(rate); + }; const expectedMintAmounts = requestDatasBefore.map((requestData, i) => requestData.depositedUsdAmount .sub(requestData.depositedUsdAmount.mul(feePercents[i]).div(10000)) .mul(constants.WeiPerEther) - .div(newExpectedRate ?? requestData.tokenOutRate), + .div(newExpectedRate(requestData)), + ); + + const estimatedMintAmounts = requestDatasBefore.map((requestData, i) => + requestData.usdAmountWithoutFees + .mul(constants.WeiPerEther) + .div(requestData.tokenOutRate), ); const groupedDataBefore = requests.map(({ id, expectedToExecute }, index) => { @@ -558,6 +827,7 @@ export const safeBulkApproveRequestTest = async ( expectedMintAmount: expectedMintAmounts[index], balance: balancesBefore[index], totalDeposited: totalDepositedsBefore[index], + estimatedMintAmount: estimatedMintAmounts[index], }; }); @@ -566,7 +836,7 @@ export const safeBulkApproveRequestTest = async ( const parsedLogs = txReceipt.logs .filter((v) => v.address === depositVault.address) .map((log) => depositVault.interface.parseLog(log)) - .filter((v) => v.name === 'SafeApproveRequest') + .filter((v) => v.name === 'ApproveRequest') .map((v) => v.args); const requestDatasAfter = await Promise.all( @@ -574,11 +844,15 @@ export const safeBulkApproveRequestTest = async ( ); const balancesAfter = await Promise.all( - requestDatasAfter.map(({ sender }) => balanceOfBase18(mTBILL, sender)), + requestDatasAfter.map(({ recipient }) => + balanceOfBase18(mTBILL, recipient), + ), ); const totalDepositedsAfter = await Promise.all( - requestDatasAfter.map(({ sender }) => depositVault.totalMinted(sender)), + requestDatasAfter.map(({ recipient }) => + depositVault.totalMinted(recipient), + ), ); const groupedDataAfter = requests.map(({ id }, index) => { @@ -605,48 +879,70 @@ export const safeBulkApproveRequestTest = async ( const totalDepositedAfter = dataAfter.totalDeposited; const totalDepositedBefore = dataBefore.totalDeposited; - expect(requestDataAfter.sender).eq(requestDataBefore.sender); + expect(requestDataAfter.depositedInstantUsdAmount).eq( + requestDataBefore.depositedInstantUsdAmount, + ); + expect(requestDataAfter.recipient).eq(requestDataBefore.recipient); expect(requestDataAfter.tokenIn).eq(requestDataBefore.tokenIn); expect(requestDataAfter.depositedUsdAmount).eq( requestDataBefore.depositedUsdAmount, ); + expect(requestDataAfter.tokenOutRate).eq(requestDataBefore.tokenOutRate); const logs = parsedLogs.filter((log) => log.requestId.eq(id)); const expectedMintedAggregatedByUser = groupedDataBefore .filter( (v) => - v.request.sender === requestDataBefore.sender && v.expectedToExecute, + v.request.recipient === requestDataBefore.recipient && + v.expectedToExecute, ) + .reduce( + (prev, curr) => { + return { + mTokenAmount: prev.mTokenAmount.add(curr.expectedMintAmount), + mintAmount: prev.mintAmount.add(curr.expectedMintAmount), + }; + }, + { mTokenAmount: BigNumber.from(0), mintAmount: BigNumber.from(0) }, + ); + + const upcomingSupplyExpectedDecrease = groupedDataBefore + .filter((v) => v.expectedToExecute) .reduce((prev, curr) => { - return prev.add(curr.expectedMintAmount); + return prev.add(curr.estimatedMintAmount); }, BigNumber.from(0)); if (!expectedToExecute) { expect(logs.length).eq(0); - expect(requestDataAfter.tokenOutRate).eq(requestDataBefore.tokenOutRate); expect(requestDataAfter.status).eq(0); + expect(requestDataAfter.approvedTokenOutRate).eq(0); } else { expect(logs.length).eq(1); - expect(requestDataAfter.tokenOutRate).eq( - newExpectedRate ?? requestDataBefore.tokenOutRate, - ); + expect(requestDataAfter.status).eq(1); + expect(totalDepositedAfter).eq( - totalDepositedBefore.add(expectedMintedAggregatedByUser), + totalDepositedBefore.add(expectedMintedAggregatedByUser.mTokenAmount), + ); + expect(requestDataAfter.approvedTokenOutRate).eq( + newExpectedRate(requestDataBefore), ); const log = logs[0]; - expect(log.newOutRate).eq( - newExpectedRate ?? requestDataBefore.tokenOutRate, - ); + expect(log.newOutRate).eq(newExpectedRate(requestDataBefore)); expect(log.requestId).eq(id); } expect(totalDepositedAfter).eq( - totalDepositedBefore.add(expectedMintedAggregatedByUser), + totalDepositedBefore.add(expectedMintedAggregatedByUser.mTokenAmount), ); - expect(balanceAfter).eq(balanceBefore.add(expectedMintedAggregatedByUser)); + expect(upcomingSupplyAfter).eq( + upcomingSupplyBefore.sub(upcomingSupplyExpectedDecrease), + ); + expect(balanceAfter).eq( + balanceBefore.add(expectedMintedAggregatedByUser.mintAmount), + ); } }; @@ -657,10 +953,13 @@ export const rejectRequestTest = async ( ) => { const sender = opt?.from ?? owner; - if (opt?.revertMessage) { - await expect( - depositVault.connect(sender).rejectRequest(requestId), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + depositVault.connect(sender).rejectRequest.bind(this, requestId), + depositVault, + opt, + ) + ) { return; } const balanceMtBillBeforeUser = await balanceOfBase18(mTBILL, sender.address); @@ -668,13 +967,31 @@ export const rejectRequestTest = async ( const totalDepositedBefore = await depositVault.totalMinted(sender.address); const requestData = await depositVault.mintRequests(requestId); + const nextExpectedRequestIdToProcessBefore = + await depositVault.nextExpectedRequestIdToProcess(); + const upcomingSupplyBefore = await depositVault.upcomingSupply(); await expect(depositVault.connect(sender).rejectRequest(requestId)) .to.emit( depositVault, - depositVault.interface.events['RejectRequest(uint256,address)'].name, + depositVault.interface.events['RejectRequest(uint256)'].name, ) - .withArgs(requestId, requestData.sender).to.not.reverted; + .withArgs(requestId).to.not.reverted; + + const nextExpectedRequestIdToProcessAfter = + await depositVault.nextExpectedRequestIdToProcess(); + + if (nextExpectedRequestIdToProcessBefore.lte(requestId)) { + expect(nextExpectedRequestIdToProcessAfter).eq( + BigNumber.from(requestId).add(1), + ); + } else { + expect(nextExpectedRequestIdToProcessAfter).eq( + nextExpectedRequestIdToProcessBefore, + ); + } + + const upcomingSupplyAfter = await depositVault.upcomingSupply(); const requestDataAfter = await depositVault.mintRequests(requestId); @@ -682,9 +999,17 @@ export const rejectRequestTest = async ( const balanceMtBillAfterUser = await balanceOfBase18(mTBILL, sender.address); + const estimatedMintAmountRequest = requestData.usdAmountWithoutFees + .mul(constants.WeiPerEther) + .div(requestData.tokenOutRate); + + expect(upcomingSupplyAfter).eq( + upcomingSupplyBefore.sub(estimatedMintAmountRequest), + ); + expect(balanceMtBillAfterUser).eq(balanceMtBillBeforeUser); expect(totalDepositedAfter).eq(totalDepositedBefore); - expect(requestDataAfter.sender).eq(requestData.sender); + expect(requestDataAfter.recipient).eq(requestData.recipient); expect(requestDataAfter.tokenIn).eq(requestData.tokenIn); expect(requestDataAfter.tokenOutRate).eq(requestData.tokenOutRate); expect(requestDataAfter.depositedUsdAmount).eq( @@ -706,10 +1031,15 @@ export const setMaxSupplyCapTest = async ( ) => { const value = parseUnits(valueN.toString()); - if (opt?.revertMessage) { - await expect( - depositVault.connect(opt?.from ?? owner).setMaxSupplyCap(value), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + depositVault + .connect(opt?.from ?? owner) + .setMaxSupplyCap.bind(this, value), + depositVault, + opt, + ) + ) { return; } @@ -717,13 +1047,47 @@ export const setMaxSupplyCapTest = async ( depositVault.connect(opt?.from ?? owner).setMaxSupplyCap(value), ).to.emit( depositVault, - depositVault.interface.events['SetMaxSupplyCap(address,uint256)'].name, + depositVault.interface.events['SetMaxSupplyCap(uint256)'].name, ).to.not.reverted; const newMax = await depositVault.maxSupplyCap(); expect(newMax).eq(value); }; +export const setMaxAmountPerRequestTest = async ( + { + depositVault, + owner, + }: { + depositVault: DepositVault | DepositVaultTest; + owner: SignerWithAddress; + }, + value: number, + opt?: OptionalCommonParams, +) => { + if ( + await handleRevert( + depositVault + .connect(opt?.from ?? owner) + .setMaxAmountPerRequest.bind(this, value), + depositVault, + opt, + ) + ) { + return; + } + + await expect( + depositVault.connect(opt?.from ?? owner).setMaxAmountPerRequest(value), + ).to.emit( + depositVault, + depositVault.interface.events['SetMaxAmountPerRequest(uint256)'].name, + ).to.not.reverted; + + const newMax = await depositVault.maxAmountPerRequest(); + expect(newMax).eq(value); +}; + export const getFeePercent = async ( sender: string, token: string, @@ -769,15 +1133,17 @@ export const calcExpectedMintAmount = async ( tokenConfig.dataFeed, sender, ); - const currentTokenIn = tokenConfig.stable + const tokenInRate = tokenConfig.stable ? constants.WeiPerEther : await dataFeedContract.getDataInBase18(); - if (currentTokenIn.isZero()) + if (tokenInRate.isZero()) return { mintAmount: constants.Zero, amountInWithoutFee: constants.Zero, actualAmountInUsd: constants.Zero, fee: constants.Zero, + usdForMintConvertion: constants.Zero, + tokenInRate: constants.Zero, }; const feePercent = await getFeePercent( @@ -792,10 +1158,10 @@ export const calcExpectedMintAmount = async ( const amountInWithoutFee = amountIn.sub(fee); - const feeInUsd = fee.mul(currentTokenIn).div(constants.WeiPerEther); + const feeInUsd = fee.mul(tokenInRate).div(constants.WeiPerEther); const actualAmountInUsd = amountIn - .mul(currentTokenIn) + .mul(tokenInRate) .div(constants.WeiPerEther); const usdForMintConvertion = actualAmountInUsd.sub(feeInUsd); @@ -803,7 +1169,9 @@ export const calcExpectedMintAmount = async ( return { mintAmount: usdForMintConvertion.mul(constants.WeiPerEther).div(mTokenRate), actualAmountInUsd, + usdForMintConvertion, amountInWithoutFee, fee, + tokenInRate, }; }; diff --git a/test/common/fast-evm.ts b/test/common/fast-evm.ts new file mode 100644 index 00000000..d2e3ffac --- /dev/null +++ b/test/common/fast-evm.ts @@ -0,0 +1,64 @@ +/** + * Test speed-up: skip the implicit `eth_estimateGas` that ethers v5 runs before + * every transaction. + * + * Profiling this suite showed the per-test cost is NOT EVM execution (an + * evm_snapshot/revert is ~0.2ms and EDR execution is sub-ms) but the JSON-RPC + * round-trips ethers v5 makes per tx. The single most expensive one is + * `eth_estimateGas`, which re-executes the entire transaction in the EVM just + * to measure gas — roughly doubling the EVM work for every tx. Measured impact + * on a transaction-heavy file: ~2.5x faster (161s -> 64s), with all tests still + * passing and no change to any test body. + * + * Instead of estimating, we return a fixed high gas cap. Correctness is + * preserved: a reverting tx still reverts at send time, so `expect(...).to.be + * .reverted*` matchers keep working (verified). gasLimit only caps execution; + * the EVM still charges the real gas used, so behaviour is identical. + * + * Opt out with FAST_EVM=false. Disabled automatically under coverage / gas + * reporting, which need real gas accounting. Set FAST_EVM_DEBUG=true to print + * how many estimateGas calls were skipped. + * + * This module is imported for its side effect from test/common/fixtures.ts so it + * engages in both serial and parallel (`--parallel`) runs, in every worker, + * before any transaction is sent. + */ +import hre from 'hardhat'; + +const disabled = + process.env.FAST_EVM === 'false' || + process.env.COVERAGE === 'true' || + process.env.REPORT_GAS === 'true'; + +if (!disabled) { + // 250M, comfortably under the 300M test block gas limit (see config/networks). + const FIXED_GAS = '0x' + (250_000_000).toString(16); + + const provider = hre.network.provider as unknown as { + send?: (method: string, params?: unknown[]) => Promise; + request?: (args: { + method: string; + params?: unknown[]; + }) => Promise; + }; + + if (typeof provider.send === 'function') { + const originalSend = provider.send.bind(provider); + provider.send = (method: string, params?: unknown[]) => { + if (method === 'eth_estimateGas') { + return Promise.resolve(FIXED_GAS); + } + return originalSend(method, params); + }; + } + + if (typeof provider.request === 'function') { + const originalRequest = provider.request.bind(provider); + provider.request = (args: { method: string; params?: unknown[] }) => { + if (args?.method === 'eth_estimateGas') { + return Promise.resolve(FIXED_GAS); + } + return originalRequest(args); + }; + } +} diff --git a/test/common/fixtures.ts b/test/common/fixtures.ts index 70e0ab6e..fa69ea20 100644 --- a/test/common/fixtures.ts +++ b/test/common/fixtures.ts @@ -1,31 +1,64 @@ +// Side-effect import: installs the eth_estimateGas interceptor before any test +// sends a transaction (see ./fast-evm). Every test loads this module via the +// fixtures, so it engages in both serial and parallel runs. Keep this first. +import './fast-evm'; + import { Options } from '@layerzerolabs/lz-v2-utilities'; -import { expect } from 'chai'; import { constants } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; -import * as hre from 'hardhat'; -import { approve, approveBase18, mintToken } from './common.helpers'; +import { NO_DELAY } from './ac.helpers'; +import { approve, approveBase18, keccak256, mintToken } from './common.helpers'; import { deployProxyContract } from './deploy.helpers'; import { addPaymentTokenTest, - addWaivedFeeAccountTest, setInstantFeeTest, setMinAmountTest, + setWaivedFeeAccountTest, } from './manageable-vault.helpers'; -import { postDeploymentTest } from './post-deploy.helpers'; - +import { + initializeDv, + initializeDvWithAave, + initializeDvWithMToken, + initializeDvWithMorpho, + initializeDvWithUstb, + initializeMv, + initializeRv, + initializeRvWithAave, + initializeRvWithMToken, + initializeRvWithMorpho, + initializeRvWithUstb, +} from './vault-initializer.helpers'; + +export { + getInitializerParamsDv, + getInitializerParamsDvWithMToken, + getInitializerParamsDvWithUstb, + getInitializerParamsMv, + getInitializerParamsRv, + getInitializerParamsRvWithMToken, + getInitializerParamsRvWithUstb, +} from './vault-initializer.helpers'; +export type { + InitializerParamsDv, + InitializerParamsDvWithMToken, + InitializerParamsDvWithUstb, + InitializerParamsMv, + InitializerParamsRv, + InitializerParamsRvWithMToken, + InitializerParamsRvWithUstb, +} from './vault-initializer.helpers'; + +import { mTokensMetadata } from '../../helpers/mtokens-metadata'; import { getAllRoles, getRolesForToken } from '../../helpers/roles'; import { AggregatorV3Mock__factory, BlacklistableTester__factory, - DepositVaultTest__factory, ERC20Mock__factory, GreenlistableTester__factory, MidasAccessControlTest__factory, PausableTester__factory, - RedemptionVaultTest__factory, - MTBILLTest__factory, WithMidasAccessControlTester__factory, DataFeedTest__factory, AggregatorV3DeprecatedMock__factory, @@ -33,21 +66,10 @@ import { CustomAggregatorV3CompatibleFeedTester__factory, SanctionsListMock__factory, WithSanctionsListTester__factory, - RedemptionTest__factory, USTBRedemptionMock__factory, - RedemptionVaultWithBUIDLTest__factory, - RedemptionVaultWithUSTBTest__factory, - RedemptionVaultWithSwapperTest__factory, - RedemptionVaultWithAaveTest__factory, - RedemptionVaultWithMorphoTest__factory, - RedemptionVaultWithMTokenTest__factory, AaveV3PoolMock__factory, MorphoVaultMock__factory, CustomAggregatorV3CompatibleFeedAdjustedTester__factory, - DepositVaultWithAaveTest__factory, - DepositVaultWithMorphoTest__factory, - DepositVaultWithMTokenTest__factory, - DepositVaultWithUSTBTest__factory, USTBMock__factory, CustomAggregatorV3CompatibleFeedGrowthTester__factory, AcreAdapter__factory, @@ -60,6 +82,11 @@ import { AxelarInterchainTokenServiceMock__factory, MidasAxelarVaultExecutableTester, LzEndpointV2Mock__factory, + MTokenTest__factory, + RedemptionVaultTest, + MidasTimelockManagerTest__factory, + MidasAccessControlTimelockControllerTest__factory, + MidasPauseManagerTest__factory, } from '../../typechain-types'; export const defaultDeploy = async () => { @@ -67,19 +94,58 @@ export const defaultDeploy = async () => { owner, customRecipient, tokensReceiver, - feeReceiver, requestRedeemer, liquidityProvider, + loanLp, + loanRepaymentAddress, + clawbackReceiver, + councilMember1, + councilMember2, + councilMember3, + councilMember4, + councilMember5, ...regularAccounts ] = await ethers.getSigners(); + const councilMembers = [ + councilMember1, + councilMember2, + councilMember3, + councilMember4, + councilMember5, + ]; + const allRoles = getAllRoles(); // main contracts const accessControl = await new MidasAccessControlTest__factory( owner, ).deploy(); - await accessControl.initialize(); + await accessControl.initialize(0, []); + + const timelockManager = await new MidasTimelockManagerTest__factory( + owner, + ).deploy(); + + await timelockManager.initialize( + accessControl.address, + 100, + councilMembers.map((v) => v.address), + ); + + const timelock = await new MidasAccessControlTimelockControllerTest__factory( + owner, + ).deploy(); + await timelock.initialize(timelockManager.address); + + const pauseManager = await new MidasPauseManagerTest__factory(owner).deploy(); + await pauseManager.initialize(accessControl.address, NO_DELAY, 86400); + + await accessControl.initializeRelationships( + timelockManager.address, + pauseManager.address, + ); + await timelockManager.initializeTimelock(timelock.address); const mockedSanctionsList = await new SanctionsListMock__factory( owner, @@ -94,13 +160,31 @@ export const defaultDeploy = async () => { mockedSanctionsList.address, ); - const mTBILL = await new MTBILLTest__factory(owner).deploy(); - await expect(mTBILL.initialize(ethers.constants.AddressZero)).to.be.reverted; - await mTBILL.initialize(accessControl.address); + const mTBILL = await new MTokenTest__factory(owner).deploy( + allRoles.tokenRoles.mTBILL.tokenManager, + allRoles.tokenRoles.mTBILL.minter, + allRoles.tokenRoles.mTBILL.burner, + ); - // separate mTBILL instance for swapper testing - const mBASIS = await new MTBILLTest__factory(owner).deploy(); - await mBASIS.initialize(accessControl.address); + await mTBILL.initialize( + accessControl.address, + clawbackReceiver.address, + mTokensMetadata.mTBILL.name, + mTokensMetadata.mTBILL.symbol, + ); + + const mTokenLoan = await new MTokenTest__factory(owner).deploy( + keccak256('M_TOKEN_MANAGER_ROLE'), + keccak256('M_TOKEN_TEST_MINT_OPERATOR_ROLE'), + keccak256('M_TOKEN_TEST_BURN_OPERATOR_ROLE'), + ); + + await mTokenLoan.initialize( + accessControl.address, + clawbackReceiver.address, + 'mTokenLoan', + 'mTokenLoan', + ); const excludedRoles = [ allRoles.common.blacklisted, @@ -114,12 +198,9 @@ export const defaultDeploy = async () => { .flat(2) .filter((v) => v !== '-' && !!v && !excludedRoles.includes(v)) as string[]; - await expect( - accessControl.grantRoleMult( - rolesFlat, - rolesFlat.map((_) => owner.address), - ), - ).not.reverted; + await accessControl.grantRoleMult( + rolesFlat.map((role) => ({ role, account: owner.address, delay: 0 })), + ); const mockedAggregator = await new AggregatorV3Mock__factory(owner).deploy(); const mockedAggregatorDecimals = await mockedAggregator.decimals(); @@ -128,6 +209,10 @@ export const defaultDeploy = async () => { owner, ).deploy(); + const mockedAggregatorMTokenLoan = await new AggregatorV3Mock__factory( + owner, + ).deploy(); + const mockedAggregatorMBasis = await new AggregatorV3Mock__factory( owner, ).deploy(); @@ -143,6 +228,10 @@ export const defaultDeploy = async () => { parseUnits('5', mockedAggregatorMTokenDecimals), ); + await mockedAggregatorMTokenLoan.setRoundData( + parseUnits('1', await mockedAggregatorMTokenLoan.decimals()), + ); + await mockedAggregatorMBasis.setRoundData( parseUnits('3', await mockedAggregatorMBasis.decimals()), ); @@ -165,6 +254,17 @@ export const defaultDeploy = async () => { parseUnits('10000', mockedAggregatorMTokenDecimals), ); + const mTokenLoanToUsdDataFeed = await new DataFeedTest__factory( + owner, + ).deploy(); + await mTokenLoanToUsdDataFeed.initialize( + accessControl.address, + mockedAggregatorMTokenLoan.address, + 3 * 24 * 3600, + parseUnits('0.1', await mockedAggregatorMTokenLoan.decimals()), + parseUnits('10000', await mockedAggregatorMTokenLoan.decimals()), + ); + const mBasisToUsdDataFeed = await new DataFeedTest__factory(owner).deploy(); await mBasisToUsdDataFeed.initialize( accessControl.address, @@ -186,68 +286,72 @@ export const defaultDeploy = async () => { parseUnits('10000'), ); - const depositVault = await new DepositVaultTest__factory(owner).deploy(); + const commonVaultParams = { + accessControl, + mockedSanctionsList, + mTBILL, + mTokenToUsdDataFeed, + tokensReceiver, + }; + + const depositVault = await initializeDv(commonVaultParams, undefined, { + from: owner, + }); - await depositVault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, + await accessControl['grantRole(bytes32,address)']( + mTBILL.minterRole(), + depositVault.address, + ); + + const redemptionVaultLoanSwapper = await initializeRv( { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, + accessControl, + mockedSanctionsList, + mTBILL: mTokenLoan, + mTokenToUsdDataFeed: mTokenLoanToUsdDataFeed, + tokensReceiver, + requestRedeemer, }, + undefined, + { from: owner }, + ); + + const redemptionVault = await initializeRv( { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), + ...commonVaultParams, + requestRedeemer, + loanLp, + loanRepaymentAddress, + redemptionVaultLoanSwapper, }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - 0, - constants.MaxUint256, + undefined, + { from: owner }, ); - await accessControl.grantRole( - mTBILL.M_TBILL_MINT_OPERATOR_ROLE(), - depositVault.address, + await accessControl['grantRole(bytes32,address)']( + mTokenLoan.burnerRole(), + redemptionVaultLoanSwapper.address, ); - const redemptionVault = await new RedemptionVaultTest__factory( - owner, - ).deploy(); + await accessControl['grantRole(bytes32,address)']( + mTokenLoan.minterRole(), + owner.address, + ); - await redemptionVault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, - }, - requestRedeemer.address, + await redemptionVaultLoanSwapper.setWaivedFeeAccount( + redemptionVault.address, + true, ); - await accessControl.grantRole( - mTBILL.M_TBILL_BURN_OPERATOR_ROLE(), + await accessControl['grantRole(bytes32,address)']( + mTBILL.burnerRole(), redemptionVault.address, ); + const manageableVault = await initializeMv(commonVaultParams, undefined, { + from: owner, + }); + const stableCoins = { usdc: await new ERC20Mock__factory(owner).deploy(8), usdt: await new ERC20Mock__factory(owner).deploy(18), @@ -259,86 +363,21 @@ export const defaultDeploy = async () => { wbtc: await new ERC20Mock__factory(owner).deploy(8), }; - /* Redemption Vault With BUIDL */ - - const buidl = await new ERC20Mock__factory(owner).deploy(8); - const buidlRedemption = await new RedemptionTest__factory(owner).deploy( - buidl.address, - stableCoins.usdc.address, - ); - await stableCoins.usdc.mint(buidlRedemption.address, parseUnits('1000000')); - - const redemptionVaultWithBUIDL = - await new RedemptionVaultWithBUIDLTest__factory(owner).deploy(); - - await redemptionVaultWithBUIDL[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address,uint256,uint256)' - ]( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, - }, - requestRedeemer.address, - buidlRedemption.address, - parseUnits('250000', 6), - parseUnits('250000', 6), - ); - await accessControl.grantRole( - mTBILL.M_TBILL_BURN_OPERATOR_ROLE(), - redemptionVaultWithBUIDL.address, - ); - const ustbToken = await new USTBMock__factory(owner).deploy(); /* Deposit Vault With USTB */ - const depositVaultWithUSTB = await new DepositVaultWithUSTBTest__factory( - owner, - ).deploy(); - - await depositVaultWithUSTB[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,uint256,uint256,address)' - ]( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, + const depositVaultWithUSTB = await initializeDvWithUstb( { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), + ...commonVaultParams, + ustbToken, }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - 0, - constants.MaxUint256, - ustbToken.address, + undefined, + { from: owner }, ); - await accessControl.grantRole( - mTBILL.M_TBILL_MINT_OPERATOR_ROLE(), + await accessControl['grantRole(bytes32,address)']( + mTBILL.minterRole(), depositVaultWithUSTB.address, ); @@ -350,40 +389,26 @@ export const defaultDeploy = async () => { ); await stableCoins.usdc.mint(ustbRedemption.address, parseUnits('1000000')); - const redemptionVaultWithUSTB = - await new RedemptionVaultWithUSTBTest__factory(owner).deploy(); - - await redemptionVaultWithUSTB[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address)' - ]( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, + const redemptionVaultWithUSTB = await initializeRvWithUstb( { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, + ...commonVaultParams, + requestRedeemer, + loanLp, + loanRepaymentAddress, + redemptionVaultLoanSwapper, + ustbRedemption, }, - requestRedeemer.address, - ustbRedemption.address, + undefined, + { from: owner }, ); - await accessControl.grantRole( - mTBILL.M_TBILL_BURN_OPERATOR_ROLE(), + await accessControl['grantRole(bytes32,address)']( + mTBILL.burnerRole(), redemptionVaultWithUSTB.address, ); + await redemptionVaultLoanSwapper.setWaivedFeeAccount( + redemptionVaultWithUSTB.address, + true, + ); /* Redemption Vault With Aave */ @@ -392,42 +417,29 @@ export const defaultDeploy = async () => { await aavePoolMock.setReserveAToken(stableCoins.usdc.address, aUSDC.address); await stableCoins.usdc.mint(aavePoolMock.address, parseUnits('1000000')); - const redemptionVaultWithAave = - await new RedemptionVaultWithAaveTest__factory(owner).deploy(); - - await redemptionVaultWithAave.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, + const redemptionVaultWithAave = await initializeRvWithAave( { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, + ...commonVaultParams, + requestRedeemer, + loanLp, + loanRepaymentAddress, + redemptionVaultLoanSwapper, }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, - }, - requestRedeemer.address, + undefined, + { from: owner }, ); await redemptionVaultWithAave.setAavePool( stableCoins.usdc.address, aavePoolMock.address, ); - await accessControl.grantRole( - mTBILL.M_TBILL_BURN_OPERATOR_ROLE(), + await accessControl['grantRole(bytes32,address)']( + mTBILL.burnerRole(), redemptionVaultWithAave.address, ); - + await redemptionVaultLoanSwapper.setWaivedFeeAccount( + redemptionVaultWithAave.address, + true, + ); /* Redemption Vault With Morpho */ const morphoVaultMock = await new MorphoVaultMock__factory(owner).deploy( @@ -435,192 +447,79 @@ export const defaultDeploy = async () => { ); await stableCoins.usdc.mint(morphoVaultMock.address, parseUnits('1000000')); - const redemptionVaultWithMorpho = - await new RedemptionVaultWithMorphoTest__factory(owner).deploy(); - - await redemptionVaultWithMorpho.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, + const redemptionVaultWithMorpho = await initializeRvWithMorpho( { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, + ...commonVaultParams, + requestRedeemer, + loanLp, + loanRepaymentAddress, + redemptionVaultLoanSwapper, }, - requestRedeemer.address, + undefined, + { from: owner }, ); await redemptionVaultWithMorpho.setMorphoVault( stableCoins.usdc.address, morphoVaultMock.address, ); - await accessControl.grantRole( - mTBILL.M_TBILL_BURN_OPERATOR_ROLE(), + await accessControl['grantRole(bytes32,address)']( + mTBILL.burnerRole(), redemptionVaultWithMorpho.address, ); - + await redemptionVaultLoanSwapper.setWaivedFeeAccount( + redemptionVaultWithMorpho.address, + true, + ); /* Deposit Vault With Aave */ - const depositVaultWithAave = await new DepositVaultWithAaveTest__factory( - owner, - ).deploy(); - - await depositVaultWithAave.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - 0, - constants.MaxUint256, + const depositVaultWithAave = await initializeDvWithAave( + commonVaultParams, + undefined, + { from: owner }, ); await depositVaultWithAave.setAavePool( stableCoins.usdc.address, aavePoolMock.address, ); - await accessControl.grantRole( - mTBILL.M_TBILL_MINT_OPERATOR_ROLE(), + await accessControl['grantRole(bytes32,address)']( + mTBILL.minterRole(), depositVaultWithAave.address, ); /* Deposit Vault With Morpho */ - const depositVaultWithMorpho = await new DepositVaultWithMorphoTest__factory( - owner, - ).deploy(); - - await depositVaultWithMorpho.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - 0, - constants.MaxUint256, + const depositVaultWithMorpho = await initializeDvWithMorpho( + commonVaultParams, + undefined, + { from: owner }, ); - await accessControl.grantRole( - mTBILL.M_TBILL_MINT_OPERATOR_ROLE(), + await accessControl['grantRole(bytes32,address)']( + mTBILL.minterRole(), depositVaultWithMorpho.address, ); /* Deposit Vault With MToken (deposits into mTBILL DV) */ - const depositVaultWithMToken = await new DepositVaultWithMTokenTest__factory( - owner, - ).deploy(); - - await depositVaultWithMToken[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,uint256,uint256,address)' - ]( - accessControl.address, + const depositVaultWithMToken = await initializeDvWithMToken( { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, + ...commonVaultParams, + depositVault, }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - 0, - constants.MaxUint256, - depositVault.address, + undefined, + { from: owner }, ); - await accessControl.grantRole( - mTBILL.M_TBILL_MINT_OPERATOR_ROLE(), + await accessControl['grantRole(bytes32,address)']( + mTBILL.minterRole(), depositVaultWithMToken.address, ); - await depositVault.addWaivedFeeAccount(depositVaultWithMToken.address); - - /* Redemption Vault With Swapper */ - - const redemptionVaultWithSwapper = - await new RedemptionVaultWithSwapperTest__factory(owner).deploy(); - - await redemptionVaultWithSwapper[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address,address)' - ]( - accessControl.address, - { - mToken: mBASIS.address, - mTokenDataFeed: mBasisToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, - }, - requestRedeemer.address, - redemptionVault.address, - liquidityProvider.address, - ); - - await redemptionVault.addWaivedFeeAccount(redemptionVaultWithSwapper.address); - await accessControl.grantRole( - mTBILL.M_TBILL_BURN_OPERATOR_ROLE(), - redemptionVaultWithSwapper.address, - ); + await depositVault.setWaivedFeeAccount(depositVaultWithMToken.address, true); /* Redemption Vault With MToken (mFONE -> mTBILL) */ - const mFONE = await new MTBILLTest__factory(owner).deploy(); - await mFONE.initialize(accessControl.address); - const mockedAggregatorMFone = await new AggregatorV3Mock__factory( owner, ).deploy(); @@ -636,44 +535,29 @@ export const defaultDeploy = async () => { parseUnits('10000', mockedAggregatorDecimals), ); - const redemptionVaultWithMToken = - await new RedemptionVaultWithMTokenTest__factory(owner).deploy(); - - await redemptionVaultWithMToken[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address)' - ]( - accessControl.address, - { - mToken: mFONE.address, - mTokenDataFeed: mFoneToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, + const redemptionVaultWithMToken = await initializeRvWithMToken( { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), + ...commonVaultParams, + requestRedeemer, + loanLp, + loanRepaymentAddress, + redemptionVaultLoanSwapper, + redemptionVault: redemptionVaultLoanSwapper.address, }, - mockedSanctionsList.address, - 1, - 1000, - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, - }, - requestRedeemer.address, - redemptionVault.address, + undefined, + { from: owner }, ); - await accessControl.grantRole( - mFONE.M_TBILL_BURN_OPERATOR_ROLE(), + await redemptionVaultLoanSwapper.setWaivedFeeAccount( redemptionVaultWithMToken.address, + true, ); - await redemptionVault.addWaivedFeeAccount(redemptionVaultWithMToken.address); - await accessControl.grantRole( - mTBILL.M_TBILL_BURN_OPERATOR_ROLE(), + await accessControl['grantRole(bytes32,address)']( + mTBILL.burnerRole(), + redemptionVaultWithMToken.address, + ); + await accessControl['grantRole(bytes32,address)']( + mTokenLoan.burnerRole(), redemptionVaultWithMToken.address, ); @@ -720,14 +604,11 @@ export const defaultDeploy = async () => { ).deploy(customFeed.address, parseUnits('10', 8)); // role granting testers - await accessControl.grantRole( - await customFeed.CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE(), + await accessControl['grantRole(bytes32,address)']( + await customFeed.contractAdminRole(), owner.address, ); - const manualFulfillmentToken = - await redemptionVault.MANUAL_FULLFILMENT_TOKEN(); - // testers const wAccessControlTester = await new WithMidasAccessControlTester__factory( owner, @@ -750,85 +631,87 @@ export const defaultDeploy = async () => { const offChainUsdToken = constants.AddressZero; // role granting testers - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( allRoles.common.blacklistedOperator, blackListableTester.address, ); - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( allRoles.common.greenlistedOperator, greenListableTester.address, ); - const greenlistToggler = await greenListableTester.greenlistTogglerRole(); - await accessControl.grantRole(greenlistToggler, owner.address); - - await postDeploymentTest(hre, { - accessControl, - aggregator: mockedAggregator, - dataFeed, - depositVault, - owner, - redemptionVault, - aggregatorMToken: mockedAggregatorMToken, - dataFeedMToken: mTokenToUsdDataFeed, - mTBILL, - minMTokenAmountForFirstDeposit: '0', - minAmount: parseUnits('100'), - tokensReceiver: tokensReceiver.address, - }); - - const mockedDeprecatedAggregator = - await new AggregatorV3DeprecatedMock__factory(owner).deploy(); - const mockedDeprecatedAggregatorDecimals = - await mockedDeprecatedAggregator.decimals(); - - await mockedDeprecatedAggregator.setRoundData( - parseUnits('5', mockedAggregatorDecimals), - ); - - await mockedDeprecatedAggregator.setRoundData( - parseUnits('1.07778', mockedAggregatorMTokenDecimals), - ); - const dataFeedDeprecated = await new DataFeedTest__factory(owner).deploy(); - await dataFeedDeprecated.initialize( - accessControl.address, - mockedDeprecatedAggregator.address, - 3 * 24 * 3600, - parseUnits('0.1', mockedDeprecatedAggregatorDecimals), - parseUnits('10000', mockedDeprecatedAggregatorDecimals), + const greenlistAdmin = await greenListableTester.greenlistAdminRole(); + await accessControl['grantRole(bytes32,address)']( + greenlistAdmin, + owner.address, ); - const mockedUnhealthyAggregator = - await new AggregatorV3UnhealthyMock__factory(owner).deploy(); - const mockedUnhealthyAggregatorDecimals = - await mockedUnhealthyAggregator.decimals(); - - await mockedUnhealthyAggregator.setRoundData( - parseUnits('5', mockedAggregatorDecimals), - ); + const deployDeprecatedFeed = async () => { + const mockedDeprecatedAggregator = + await new AggregatorV3DeprecatedMock__factory(owner).deploy(); + const mockedDeprecatedAggregatorDecimals = + await mockedDeprecatedAggregator.decimals(); + + await mockedDeprecatedAggregator.setRoundData( + parseUnits('5', mockedAggregatorDecimals), + ); + + await mockedDeprecatedAggregator.setRoundData( + parseUnits('1.07778', mockedAggregatorMTokenDecimals), + ); + const dataFeedDeprecated = await new DataFeedTest__factory(owner).deploy(); + await dataFeedDeprecated.initialize( + accessControl.address, + mockedDeprecatedAggregator.address, + 3 * 24 * 3600, + parseUnits('0.1', mockedDeprecatedAggregatorDecimals), + parseUnits('10000', mockedDeprecatedAggregatorDecimals), + ); + + return { + dataFeedDeprecated, + mockedDeprecatedAggregator, + mockedDeprecatedAggregatorDecimals, + }; + }; - await mockedUnhealthyAggregator.setRoundData( - parseUnits('1.07778', mockedAggregatorMTokenDecimals), - ); - const dataFeedUnhealthy = await new DataFeedTest__factory(owner).deploy(); - await dataFeedUnhealthy.initialize( - accessControl.address, - mockedUnhealthyAggregator.address, - 3 * 24 * 3600, - parseUnits('0.1', mockedUnhealthyAggregatorDecimals), - parseUnits('10000', mockedUnhealthyAggregatorDecimals), - ); + const deployUnhealthyFeed = async () => { + const mockedUnhealthyAggregator = + await new AggregatorV3UnhealthyMock__factory(owner).deploy(); + const mockedUnhealthyAggregatorDecimals = + await mockedUnhealthyAggregator.decimals(); + + await mockedUnhealthyAggregator.setRoundData( + parseUnits('5', mockedAggregatorDecimals), + ); + + await mockedUnhealthyAggregator.setRoundData( + parseUnits('1.07778', mockedAggregatorMTokenDecimals), + ); + const dataFeedUnhealthy = await new DataFeedTest__factory(owner).deploy(); + await dataFeedUnhealthy.initialize( + accessControl.address, + mockedUnhealthyAggregator.address, + 3 * 24 * 3600, + parseUnits('0.1', mockedUnhealthyAggregatorDecimals), + parseUnits('10000', mockedUnhealthyAggregatorDecimals), + ); + + return { + dataFeedUnhealthy, + mockedUnhealthyAggregator, + mockedUnhealthyAggregatorDecimals, + }; + }; return { customFeed, customFeedAdjusted, customFeedGrowth, mTBILL, - mBASIS, - redemptionVaultWithSwapper, mBasisToUsdDataFeed, accessControl, wAccessControlTester, - roles: { ...allRoles, greenlistToggler }, + roles: { ...allRoles, greenlistAdmin }, owner, regularAccounts, blackListableTester, @@ -840,22 +723,15 @@ export const defaultDeploy = async () => { depositVault, redemptionVault, stableCoins, - manualFulfillmentToken, mTokenToUsdDataFeed, mockedAggregatorMToken, mockedAggregatorMBasis, offChainUsdToken, mockedAggregatorMTokenDecimals, tokensReceiver, - feeReceiver, - dataFeedDeprecated, - dataFeedUnhealthy, withSanctionsListTester, mockedSanctionsList, requestRedeemer, - buidl, - buidlRedemption, - redemptionVaultWithBUIDL, redemptionVaultWithUSTB, redemptionVaultWithAave, aavePoolMock, @@ -863,7 +739,6 @@ export const defaultDeploy = async () => { redemptionVaultWithMorpho, morphoVaultMock, liquidityProvider, - mFONE, mockedAggregatorMFone, mFoneToUsdDataFeed, redemptionVaultWithMToken, @@ -877,9 +752,31 @@ export const defaultDeploy = async () => { depositVaultWithMToken, dataFeedGrowth, compositeDataFeed, + loanLp, + loanRepaymentAddress, + redemptionVaultLoanSwapper, + mTokenLoan, + mTokenLoanToUsdDataFeed, + mockedAggregatorMTokenLoan, + timelock, + timelockManager, + pauseManager, + councilMembers, + clawbackReceiver, + manageableVault, + tokenContract: mTBILL, + deployDeprecatedFeed, + deployUnhealthyFeed, }; }; +export type DefaultFixture = Omit< + Awaited>, + 'redemptionVault' +> & { + redemptionVault: RedemptionVaultTest; +}; + /** * mTokenPermissionedTest + dedicated deposit/redemption vaults (for integration-style tests). */ @@ -891,7 +788,6 @@ export const mTokenPermissionedFixture = async ( owner, accessControl, mockedSanctionsList, - feeReceiver, tokensReceiver, requestRedeemer, mTokenToUsdDataFeed, @@ -900,73 +796,58 @@ export const mTokenPermissionedFixture = async ( const mTokenPermissioned = await new MTokenPermissionedTest__factory( owner, ).deploy(); - await mTokenPermissioned.initialize(accessControl.address); + await mTokenPermissioned.initialize( + accessControl.address, + fx.clawbackReceiver.address, + 'mTokenPermissioned', + 'mTokenPermissioned', + ); - const mintRole = await mTokenPermissioned.M_TOKEN_TEST_MINT_OPERATOR_ROLE(); - const burnRole = await mTokenPermissioned.M_TOKEN_TEST_BURN_OPERATOR_ROLE(); - const pauseRole = await mTokenPermissioned.M_TOKEN_TEST_PAUSE_OPERATOR_ROLE(); + const mintRole = await mTokenPermissioned.minterRole(); + const burnRole = await mTokenPermissioned.burnerRole(); + const tokenManagerRole = await mTokenPermissioned.contractAdminRole(); const mTokenPermissionedGreenlistedRole = - await mTokenPermissioned.M_TOKEN_TEST_GREENLISTED_ROLE(); + await mTokenPermissioned.greenlistedRole(); - await accessControl.grantRole(mintRole, owner.address); - await accessControl.grantRole(burnRole, owner.address); - await accessControl.grantRole(pauseRole, owner.address); + await accessControl['grantRole(bytes32,address)'](mintRole, owner.address); + await accessControl['grantRole(bytes32,address)'](burnRole, owner.address); + await accessControl['grantRole(bytes32,address)']( + tokenManagerRole, + owner.address, + ); - const mTokenPermissionedDepositVault = await new DepositVaultTest__factory( - owner, - ).deploy(); - await mTokenPermissionedDepositVault.initialize( - accessControl.address, + const mTokenPermissionedDepositVault = await initializeDv( { - mToken: mTokenPermissioned.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, + accessControl, + mockedSanctionsList, + mTBILL: mTokenPermissioned, + mTokenToUsdDataFeed, + tokensReceiver, }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - 0, - constants.MaxUint256, + undefined, + { from: owner }, ); - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( mintRole, mTokenPermissionedDepositVault.address, ); - const mTokenPermissionedRedemptionVault = - await new RedemptionVaultTest__factory(owner).deploy(); - await mTokenPermissionedRedemptionVault.initialize( - accessControl.address, - { - mToken: mTokenPermissioned.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, + const mTokenPermissionedRedemptionVault = await initializeRv( { - instantFee: 0, - instantDailyLimit: parseUnits('100000'), + accessControl, + mockedSanctionsList, + mTBILL: mTokenPermissioned, + mTokenToUsdDataFeed, + tokensReceiver, + requestRedeemer, }, - mockedSanctionsList.address, - 1, - 1000, - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, - }, - requestRedeemer.address, + undefined, + { from: owner }, ); - await accessControl.grantRole( + + await mTokenPermissionedRedemptionVault.setMaxApproveRequestId(100); + + await accessControl['grantRole(bytes32,address)']( burnRole, mTokenPermissionedRedemptionVault.address, ); @@ -977,7 +858,7 @@ export const mTokenPermissionedFixture = async ( mTokenPermissionedRoles: { mint: mintRole, burn: burnRole, - pause: pauseRole, + manager: tokenManagerRole, greenlisted: mTokenPermissionedGreenlistedRole, }, mTokenPermissionedDepositVault, @@ -1015,9 +896,10 @@ export const acreAdapterFixture = async () => { 0, ); - await addWaivedFeeAccountTest( + await setWaivedFeeAccountTest( { vault: defaultFixture.redemptionVault, owner: defaultFixture.owner }, acreUsdcMTbillAdapter.address, + true, ); await addPaymentTokenTest( @@ -1109,15 +991,12 @@ export const layerZeroFixture = async () => { }, ]); - await accessControl.grantRoleMult( - [roles.minter, roles.burner, roles.minter, roles.burner], - [ - oftAdapterA.address, - oftAdapterA.address, - oftAdapterB.address, - oftAdapterB.address, - ], - ); + await accessControl.grantRoleMult([ + { role: roles.minter, account: oftAdapterA.address, delay: 0 }, + { role: roles.burner, account: oftAdapterA.address, delay: 0 }, + { role: roles.minter, account: oftAdapterB.address, delay: 0 }, + { role: roles.burner, account: oftAdapterB.address, delay: 0 }, + ]); await mockEndpointA.setDestLzEndpoint( oftAdapterB.address, @@ -1313,15 +1192,12 @@ export const axelarFixture = async () => { const roles = getRolesForToken('mTBILL'); - await accessControl.grantRoleMult( - [roles.minter, roles.burner, roles.minter, roles.burner], - [ - axelarItsA.address, - axelarItsA.address, - axelarItsB.address, - axelarItsB.address, - ], - ); + await accessControl.grantRoleMult([ + { role: roles.minter, account: axelarItsA.address, delay: 0 }, + { role: roles.burner, account: axelarItsA.address, delay: 0 }, + { role: roles.minter, account: axelarItsB.address, delay: 0 }, + { role: roles.burner, account: axelarItsB.address, delay: 0 }, + ]); const executor = await deployProxyContract( 'MidasAxelarVaultExecutableTester', diff --git a/test/common/greenlist.helpers.ts b/test/common/greenlist.helpers.ts index 3bc5ac27..e7171e36 100644 --- a/test/common/greenlist.helpers.ts +++ b/test/common/greenlist.helpers.ts @@ -1,7 +1,7 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { OptionalCommonParams } from './common.helpers'; +import { handleRevert, OptionalCommonParams } from './common.helpers'; import { Greenlistable } from '../../typechain-types'; @@ -15,10 +15,15 @@ export const greenListEnable = async ( enable: boolean, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - greenlistable.connect(opt?.from ?? owner).setGreenlistEnable(enable), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + greenlistable + .connect(opt?.from ?? owner) + .setGreenlistEnable.bind(this, enable), + greenlistable, + opt, + ) + ) { return; } @@ -26,7 +31,7 @@ export const greenListEnable = async ( greenlistable.connect(opt?.from ?? owner).setGreenlistEnable(enable), ).to.emit( greenlistable, - greenlistable.interface.events['SetGreenlistEnable(address,bool)'].name, + greenlistable.interface.events['SetGreenlistEnable(bool)'].name, ).to.not.reverted; expect(await greenlistable.greenlistEnabled()).eq(enable); diff --git a/test/common/layerzero.helpers.ts b/test/common/layerzero.helpers.ts index 98e3f8db..d364160c 100644 --- a/test/common/layerzero.helpers.ts +++ b/test/common/layerzero.helpers.ts @@ -4,13 +4,16 @@ import { BigNumber, BigNumberish, constants, - Contract, ContractTransaction, } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; -import { OptionalCommonParams, tokenAmountToBase18 } from './common.helpers'; +import { + handleRevert, + OptionalCommonParams, + tokenAmountToBase18, +} from './common.helpers'; import { calcExpectedMintAmount } from './deposit-vault.helpers'; import { layerZeroFixture } from './fixtures'; import { calcExpectedTokenOutAmount } from './redemption-vault.helpers'; @@ -60,10 +63,11 @@ export const setRateLimitConfig = async ( ) => { const from = opt?.from ?? owner; - if (opt?.revertMessage) { - await expect( - oftAdapter.connect(from).setRateLimits([{ dstEid, limit, window }]), - ).revertedWith(opt?.revertMessage); + const callFn = oftAdapter + .connect(from) + .setRateLimits.bind(this, [{ dstEid, limit, window }]); + + if (await handleRevert(callFn, oftAdapter, opt)) { return; } @@ -84,10 +88,6 @@ export const sendOft = async ( }, opt?: { revertOnDst?: boolean; - revertWithCustomError?: { - contract: Contract; - error: string; - }; } & OptionalCommonParams, ) => { const from = opt?.from ?? owner; @@ -130,20 +130,9 @@ export const sendOft = async ( { value: nativeFee }, ] as const; - if (opt?.revertMessage && !opt?.revertOnDst) { - await expect( - oftFrom.connect(opt?.from ?? owner).send(...params), - ).revertedWith(opt?.revertMessage); - return; - } + const callFn = oftFrom.connect(opt?.from ?? owner).send.bind(this, ...params); - if (opt?.revertWithCustomError && !opt?.revertOnDst) { - await expect( - oftFrom.connect(opt?.from ?? owner).send(...params), - ).revertedWithCustomError( - opt?.revertWithCustomError.contract, - opt?.revertWithCustomError.error, - ); + if (!opt?.revertOnDst && (await handleRevert(callFn, oftFrom, opt))) { return; } @@ -155,7 +144,7 @@ export const sendOft = async ( oftFrom, oftFrom.interface.events['OFTSent(bytes32,uint32,address,uint256,uint256)'] .name, - ).not.reverted; + ); const totalSupplyAfter = await mTBILL.totalSupply(); const balanceFromAfter = await mTBILL.balanceOf(from.address); @@ -203,10 +192,6 @@ export const sendOftLockBox = async ( }, opt?: { revertOnDst?: boolean; - revertWithCustomError?: { - contract: Contract; - error: string; - }; } & OptionalCommonParams, ) => { const from = opt?.from ?? owner; @@ -249,20 +234,9 @@ export const sendOftLockBox = async ( { value: nativeFee }, ] as const; - if (opt?.revertMessage && !opt?.revertOnDst) { - await expect( - oftFrom.connect(opt?.from ?? owner).send(...params), - ).revertedWith(opt?.revertMessage); - return; - } + const callFn = oftFrom.connect(opt?.from ?? owner).send.bind(this, ...params); - if (opt?.revertWithCustomError && !opt?.revertOnDst) { - await expect( - oftFrom.connect(opt?.from ?? owner).send(...params), - ).revertedWithCustomError( - opt?.revertWithCustomError.contract, - opt?.revertWithCustomError.error, - ); + if (!opt?.revertOnDst && (await handleRevert(callFn, oftFrom, opt))) { return; } @@ -277,7 +251,7 @@ export const sendOftLockBox = async ( oftFrom, oftFrom.interface.events['OFTSent(bytes32,uint32,address,uint256,uint256)'] .name, - ).not.reverted; + ); const totalSupplyAfter = await pToken.totalSupply(); const totalSupplyOftAfter = await pTokenLzOft.totalSupply(); @@ -333,10 +307,6 @@ export const depositAndSend = async ( opt?: { revertOnDst?: boolean; refundOnDst?: boolean; - revertWithCustomError?: { - contract: Contract; - error: string; - }; overrideValue?: BigNumberish; expectedMintAmountWoDust?: BigNumberish; } & OptionalCommonParams, @@ -491,16 +461,9 @@ export const depositAndSend = async ( txFn = composer.connect(from).depositAndSend.bind(this, ...params); } - if (opt?.revertMessage && !opt?.revertOnDst) { - await expect(txFn()).revertedWith(opt?.revertMessage); - return; - } + const callFn = composer.connect(from).depositAndSend.bind(this, ...params); - if (opt?.revertWithCustomError && !opt?.revertOnDst) { - await expect(txFn()).revertedWithCustomError( - opt?.revertWithCustomError.contract, - opt?.revertWithCustomError.error, - ); + if (!opt?.revertOnDst && (await handleRevert(callFn, composer, opt))) { return; } @@ -515,31 +478,12 @@ export const depositAndSend = async ( composer.interface.events['Refunded(bytes32)'].name, ); } else { - await expect(txFn()) - .to.emit( - composer, - composer.interface.events[ - 'Deposited(bytes32,bytes32,uint32,uint256,uint256)' - ].name, - ) - .to.emit( - depositVault, - depositVault.interface.events[ - 'DepositInstantWithCustomRecipient(address,address,address,uint256,uint256,uint256,uint256,bytes32)' - ].name, - ) - .withArgs( - composer.address, - await composer.paymentTokenErc20(), - direction === 'A_TO_A' || direction === 'B_TO_A' - ? recipient - : composer.address, - actualAmountInUsd, - amountParsed, - fee, - mintAmount, - referrerId, - ); + await expect(txFn()).to.emit( + composer, + composer.interface.events[ + 'Deposited(bytes32,bytes32,uint32,uint256,uint256)' + ].name, + ); } const totalSupplyAfter = await mTBILL.totalSupply(); @@ -605,10 +549,7 @@ export const redeemAndSend = async ( refundOnDst?: boolean; expectedReceiveAmountWoDust?: BigNumberish; revertOnDst?: boolean; - revertWithCustomError?: { - contract: Contract; - error: string; - }; + overrideValue?: BigNumberish; } & OptionalCommonParams, ) => { @@ -749,16 +690,9 @@ export const redeemAndSend = async ( txFn = composer.connect(from).redeemAndSend.bind(this, ...params); } - if (opt?.revertMessage && !opt?.revertOnDst) { - await expect(txFn()).revertedWith(opt?.revertMessage); - return; - } + const callFn = composer.connect(from).redeemAndSend.bind(this, ...params); - if (opt?.revertWithCustomError && !opt?.revertOnDst) { - await expect(txFn()).revertedWithCustomError( - opt?.revertWithCustomError.contract, - opt?.revertWithCustomError.error, - ); + if (!opt?.revertOnDst && (await handleRevert(callFn, composer, opt))) { return; } @@ -788,22 +722,6 @@ export const redeemAndSend = async ( direction === 'A_TO_A' || direction === 'B_TO_A' ? eidA : eidB, amountParsed, amountOut, - ) - .to.emit( - redemptionVault, - redemptionVault.interface.events[ - 'RedeemInstantWithCustomRecipient(address,address,address,uint256,uint256,uint256)' - ].name, - ) - .withArgs( - composer.address, - await composer.paymentTokenErc20(), - direction === 'A_TO_A' || direction === 'B_TO_A' - ? recipient - : composer.address, - amountParsed, - fee, - amountOut, ); } diff --git a/test/common/mTBILL.helpers.ts b/test/common/mTBILL.helpers.ts deleted file mode 100644 index 8613a8b3..00000000 --- a/test/common/mTBILL.helpers.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { expect } from 'chai'; -import { BigNumberish } from 'ethers'; -import { defaultAbiCoder, solidityKeccak256 } from 'ethers/lib/utils'; - -import { Account, OptionalCommonParams, getAccount } from './common.helpers'; - -import { MTBILL, MToken, MTokenPermissioned } from '../../typechain-types'; - -type CommonParams = { - tokenContract: MToken | MTBILL | MTokenPermissioned; - owner: SignerWithAddress; -}; - -export const setMetadataTest = async ( - { tokenContract, owner }: CommonParams, - key: string, - value: string, - opt?: OptionalCommonParams, -) => { - const keyBytes32 = solidityKeccak256(['string'], [key]); - const valueBytes = defaultAbiCoder.encode(['string'], [value]); - - if (opt?.revertMessage) { - await expect( - tokenContract - .connect(opt?.from ?? owner) - .setMetadata(keyBytes32, valueBytes), - ).revertedWith(opt?.revertMessage); - return; - } - - await expect( - tokenContract - .connect(opt?.from ?? owner) - .setMetadata(keyBytes32, valueBytes), - ).not.reverted; - - expect(await tokenContract.metadata(keyBytes32)).eq(valueBytes); -}; - -export const mint = async ( - { tokenContract, owner }: CommonParams, - to: Account, - amount: BigNumberish, - opt?: OptionalCommonParams, -) => { - to = getAccount(to); - - if (opt?.revertMessage) { - await expect( - tokenContract.connect(opt?.from ?? owner).mint(to, amount), - ).revertedWith(opt?.revertMessage); - return; - } - - const balanceBefore = await tokenContract.balanceOf(to); - - await expect(tokenContract.connect(owner).mint(to, amount)).to.emit( - tokenContract, - tokenContract.interface.events['Transfer(address,address,uint256)'].name, - ).to.not.reverted; - - const balanceAfter = await tokenContract.balanceOf(to); - - expect(balanceAfter.sub(balanceBefore)).eq(amount); -}; - -export const burn = async ( - { tokenContract, owner }: CommonParams, - from: Account, - amount: BigNumberish, - opt?: OptionalCommonParams, -) => { - from = getAccount(from); - - if (opt?.revertMessage) { - await expect( - tokenContract.connect(opt?.from ?? owner).burn(from, amount), - ).revertedWith(opt?.revertMessage); - return; - } - - const balanceBefore = await tokenContract.balanceOf(from); - - await expect(tokenContract.connect(owner).burn(from, amount)).to.emit( - tokenContract, - tokenContract.interface.events['Transfer(address,address,uint256)'].name, - ).to.not.reverted; - - const balanceAfter = await tokenContract.balanceOf(from); - - expect(balanceBefore.sub(balanceAfter)).eq(amount); -}; diff --git a/test/common/manageable-vault.helpers.ts b/test/common/manageable-vault.helpers.ts index 2ac6ba78..4167a841 100644 --- a/test/common/manageable-vault.helpers.ts +++ b/test/common/manageable-vault.helpers.ts @@ -1,9 +1,15 @@ +import { days } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time/duration'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; import { BigNumber, BigNumberish, constants } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; -import { Account, OptionalCommonParams, getAccount } from './common.helpers'; +import { + getAccount, + getCurrentBlockTimestamp, + handleRevert, + OptionalCommonParams, +} from './common.helpers'; import { defaultDeploy } from './fixtures'; import { @@ -15,12 +21,11 @@ import { ERC20, ERC20__factory, IERC20, + ManageableVault, RedemptionVault, - RedemptionVaultWIthBUIDL, RedemptionVaultWithAave, RedemptionVaultWithMorpho, RedemptionVaultWithMToken, - RedemptionVaultWithSwapper, RedemptionVaultWithUSTB, } from '../../typechain-types'; @@ -32,12 +37,11 @@ type CommonParamsChangePaymentToken = { | DepositVaultWithMToken | DepositVaultWithUSTB | RedemptionVault - | RedemptionVaultWIthBUIDL | RedemptionVaultWithAave | RedemptionVaultWithMorpho | RedemptionVaultWithMToken - | RedemptionVaultWithSwapper - | RedemptionVaultWithUSTB; + | RedemptionVaultWithUSTB + | ManageableVault; owner: SignerWithAddress; }; type CommonParams = { @@ -49,75 +53,151 @@ type CommonParams = { | DepositVaultWithUSTB; } & Pick>, 'owner'>; +export type WindowRateLimitCapacityParams = { + /** stored `amountInFlight` (not a pre-decayed view) */ + amountInFlight: BigNumberish; + lastUpdated: BigNumberish; + limit: BigNumberish; + window: BigNumberish; + /** current timestamp (`block.timestamp`) */ + now: BigNumberish; +}; + +export type WindowRateLimitCapacity = { + inFlight: BigNumber; + remaining: BigNumber; +}; + +/** `Math.mulDiv(a, b, c, Down)` — matches OpenZeppelin / `RateLimitLibrary`. */ +export const mulDiv = ( + a: BigNumberish, + b: BigNumberish, + c: BigNumberish, +): BigNumber => { + const denominator = BigNumber.from(c); + if (denominator.isZero()) { + return BigNumber.from(0); + } + return BigNumber.from( + (BigInt(BigNumber.from(a).toString()) * + BigInt(BigNumber.from(b).toString())) / + BigInt(denominator.toString()), + ); +}; + +/** + * Mirrors `RateLimitLibrary._availableCapacity` (decayed in-flight + headroom). + */ +export const calculateWindowRateLimitCapacity = ({ + amountInFlight, + lastUpdated, + limit, + window, + now, +}: WindowRateLimitCapacityParams): WindowRateLimitCapacity => { + const elapsed = BigNumber.from(now).sub(lastUpdated); + const windowBn = BigNumber.from(window); + const divisor = windowBn.isZero() ? BigNumber.from(1) : windowBn; + + const decay = mulDiv(limit, elapsed, divisor); + + const amountInFlightBn = BigNumber.from(amountInFlight); + const inFlight = amountInFlightBn.lte(decay) + ? BigNumber.from(0) + : amountInFlightBn.sub(decay); + + const limitBn = BigNumber.from(limit); + const remaining = limitBn.lte(inFlight) + ? BigNumber.from(0) + : limitBn.sub(inFlight); + + return { inFlight, remaining }; +}; + export const setInstantFeeTest = async ( { vault, owner }: CommonParamsChangePaymentToken, newFee: BigNumberish, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).setInstantFee(newFee), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + vault.connect(opt?.from ?? owner).setInstantFee.bind(this, newFee), + vault, + opt, + ) + ) { return; } await expect(vault.connect(opt?.from ?? owner).setInstantFee(newFee)) - .to.emit( - vault, - vault.interface.events['SetInstantFee(address,uint256)'].name, - ) - .withArgs((opt?.from ?? owner).address, newFee).to.not.reverted; + .to.emit(vault, vault.interface.events['SetInstantFee(uint256)'].name) + .withArgs(newFee); const fee = await vault.instantFee(); expect(fee).eq(newFee); }; -export const setVariabilityToleranceTest = async ( +export const setMinMaxInstantFeeTest = async ( { vault, owner }: CommonParamsChangePaymentToken, - newTolerance: BigNumberish, + newMinInstantFee: BigNumberish, + newMaxInstantFee: BigNumberish, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).setVariationTolerance(newTolerance), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + vault + .connect(opt?.from ?? owner) + .setMinMaxInstantFee.bind(this, newMinInstantFee, newMaxInstantFee), + vault, + opt, + ) + ) { return; } await expect( - vault.connect(opt?.from ?? owner).setVariationTolerance(newTolerance), + vault + .connect(opt?.from ?? owner) + .setMinMaxInstantFee(newMinInstantFee, newMaxInstantFee), ) .to.emit( vault, - vault.interface.events['SetVariationTolerance(address,uint256)'].name, + vault.interface.events['SetMinMaxInstantFee(uint256,uint256)'].name, ) - .withArgs((opt?.from ?? owner).address, newTolerance).to.not.reverted; + .withArgs(newMinInstantFee, newMaxInstantFee); - const tolerance = await vault.variationTolerance(); - expect(tolerance).eq(newTolerance); + expect(await vault.minInstantFee()).eq(newMinInstantFee); + expect(await vault.maxInstantFee()).eq(newMaxInstantFee); }; -export const addWaivedFeeAccountTest = async ( +export const setVariabilityToleranceTest = async ( { vault, owner }: CommonParamsChangePaymentToken, - account: string, + newTolerance: BigNumberish, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).addWaivedFeeAccount(account), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + vault + .connect(opt?.from ?? owner) + .setVariationTolerance.bind(this, newTolerance), + vault, + opt, + ) + ) { return; } - await expect(vault.connect(opt?.from ?? owner).addWaivedFeeAccount(account)) + await expect( + vault.connect(opt?.from ?? owner).setVariationTolerance(newTolerance), + ) .to.emit( vault, - vault.interface.events['AddWaivedFeeAccount(address,address)'].name, + vault.interface.events['SetVariationTolerance(uint256)'].name, ) - .withArgs((opt?.from ?? owner).address, account).to.not.reverted; + .withArgs(newTolerance); - const isWaivedFee = await vault.waivedFeeRestriction(account); - expect(isWaivedFee).eq(true); + const tolerance = await vault.variationTolerance(); + expect(tolerance).eq(newTolerance); }; export const changeTokenAllowanceTest = async ( @@ -126,12 +206,15 @@ export const changeTokenAllowanceTest = async ( newAllowance: BigNumberish, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( vault .connect(opt?.from ?? owner) - .changeTokenAllowance(token, newAllowance), - ).revertedWith(opt?.revertMessage); + .changeTokenAllowance.bind(this, token, newAllowance), + vault, + opt, + ) + ) { return; } @@ -140,10 +223,9 @@ export const changeTokenAllowanceTest = async ( ) .to.emit( vault, - vault.interface.events['ChangeTokenAllowance(address,address,uint256)'] - .name, + vault.interface.events['ChangeTokenAllowance(address,uint256)'].name, ) - .withArgs((opt?.from ?? owner).address, token).to.not.reverted; + .withArgs(token, newAllowance); const allowance = (await vault.tokensConfig(token)).allowance; expect(allowance).eq(newAllowance); @@ -155,93 +237,217 @@ export const changeTokenFeeTest = async ( newFee: BigNumberish, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).changeTokenFee(token, newFee), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + vault + .connect(opt?.from ?? owner) + .changeTokenFee.bind(this, token, newFee), + vault, + opt, + ) + ) { return; } await expect(vault.connect(opt?.from ?? owner).changeTokenFee(token, newFee)) .to.emit( vault, - vault.interface.events['ChangeTokenFee(address,address,uint256)'].name, + vault.interface.events['ChangeTokenFee(address,uint256)'].name, ) - .withArgs((opt?.from ?? owner).address, token, newFee).to.not.reverted; + .withArgs(token, newFee); const fee = (await vault.tokensConfig(token)).fee; expect(fee).eq(newFee); }; -export const removeWaivedFeeAccountTest = async ( +export const setWaivedFeeAccountTest = async ( { vault, owner }: CommonParamsChangePaymentToken, account: string, + enable: boolean, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).removeWaivedFeeAccount(account), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + vault + .connect(opt?.from ?? owner) + .setWaivedFeeAccount.bind(this, account, enable), + vault, + opt, + ) + ) { return; } await expect( - vault.connect(opt?.from ?? owner).removeWaivedFeeAccount(account), + vault.connect(opt?.from ?? owner).setWaivedFeeAccount(account, enable), ) .to.emit( vault, - vault.interface.events['RemoveWaivedFeeAccount(address,address)'].name, + vault.interface.events['SetWaivedFeeAccount(address,bool)'].name, ) - .withArgs((opt?.from ?? owner).address, account).to.not.reverted; + .withArgs(account, enable); const isWaivedFee = await vault.waivedFeeRestriction(account); - expect(isWaivedFee).eq(false); + expect(isWaivedFee).eq(enable); }; -export const setInstantDailyLimitTest = async ( +export const setInstantLimitConfigTest = async ( { vault, owner }: CommonParamsChangePaymentToken, - newLimit: BigNumberish, + newLimit: BigNumberish | { window: BigNumberish; limit: BigNumberish }, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).setInstantDailyLimit(newLimit), - ).revertedWith(opt?.revertMessage); + const { window, limit: newLimitValue } = + typeof newLimit === 'object' && 'window' in newLimit + ? { window: newLimit.window, limit: newLimit.limit } + : { window: days(1), limit: newLimit }; + + if ( + await handleRevert( + vault + .connect(opt?.from ?? owner) + .setInstantLimitConfig.bind(this, window, newLimitValue), + vault, + opt, + ) + ) { return; } - await expect(vault.connect(opt?.from ?? owner).setInstantDailyLimit(newLimit)) - .to.emit( + const limitConfigsBefore = await vault.getInstantLimitStatuses(); + + await expect( + vault + .connect(opt?.from ?? owner) + .setInstantLimitConfig(window, newLimitValue), + ) + .to.emit(vault, 'WindowLimitSet') + .withArgs(window, newLimitValue); + + const limitConfigsAfter = await vault.getInstantLimitStatuses(); + + const configBefore = limitConfigsBefore.filter((w) => + w.window.eq(window), + )?.[0]; + + const configAfter = limitConfigsAfter.filter((w) => w.window.eq(window))?.[0]; + + const currentTimestamp = await getCurrentBlockTimestamp(); + + if (configBefore) { + const { inFlight, remaining } = calculateWindowRateLimitCapacity({ + amountInFlight: configBefore.inFlight, + lastUpdated: configBefore.lastUpdated, + limit: newLimitValue, + window, + now: currentTimestamp, + }); + + expect(configAfter).not.eq(undefined); + expect(configBefore).not.eq(undefined); + expect(configAfter.limit).eq(newLimitValue); + expect(configAfter.lastUpdated).eq(currentTimestamp); + expect(configAfter.inFlight).eq(inFlight); + expect(configAfter.remaining).eq(remaining); + } else { + expect(configAfter).not.eq(undefined); + expect(configBefore).eq(undefined); + expect(configAfter.limit).eq(newLimitValue); + expect(configAfter.inFlight).eq(0); + expect(configAfter.lastUpdated).eq(currentTimestamp); + expect(configAfter.remaining).eq(newLimitValue); + } +}; + +export const removeInstantLimitConfigTest = async ( + { vault, owner }: CommonParamsChangePaymentToken, + window: BigNumberish, + opt?: OptionalCommonParams, +) => { + if ( + await handleRevert( + vault + .connect(opt?.from ?? owner) + .removeInstantLimitConfig.bind(this, window), vault, - vault.interface.events['SetInstantDailyLimit(address,uint256)'].name, + opt, ) - .withArgs((opt?.from ?? owner).address, newLimit).to.not.reverted; + ) { + return; + } + + const limitConfigsBefore = await vault.getInstantLimitStatuses(); + const indexBefore = limitConfigsBefore.findIndex((w) => w.window.eq(window)); + expect(indexBefore).gte( + 0, + 'removeInstantLimitConfigTest: window must exist before removal', + ); + + await expect( + vault.connect(opt?.from ?? owner).removeInstantLimitConfig(window), + ) + .to.emit(vault, 'WindowLimitRemoved') + .withArgs(window); - const limit = await vault.instantDailyLimit(); - expect(limit).eq(newLimit); + const limitConfigsAfter = await vault.getInstantLimitStatuses(); + expect(limitConfigsAfter.length).eq(limitConfigsBefore.length - 1); + expect(limitConfigsAfter.filter((w) => w.window.eq(window)).length).eq(0); }; -export const setFeeReceiverTest = async ( +export const setMaxApproveRequestIdTest = async ( { vault, owner }: CommonParamsChangePaymentToken, - newReceiver: string, + maxApproveRequestId: BigNumberish, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).setFeeReceiver(newReceiver), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + vault + .connect(opt?.from ?? owner) + .setMaxApproveRequestId.bind(this, maxApproveRequestId), + vault, + opt, + ) + ) { return; } - await expect(vault.connect(opt?.from ?? owner).setFeeReceiver(newReceiver)) + await expect( + vault + .connect(opt?.from ?? owner) + .setMaxApproveRequestId(maxApproveRequestId), + ) .to.emit( vault, - vault.interface.events['SetFeeReceiver(address,address)'].name, + vault.interface.events['SetMaxApproveRequestId(uint256)'].name, + ) + .withArgs(maxApproveRequestId); + + const newMaxApproveRequestId = await vault.maxApproveRequestId(); + expect(newMaxApproveRequestId).eq(maxApproveRequestId); +}; + +export const setMaxInstantShareTest = async ( + { vault, owner }: CommonParamsChangePaymentToken, + maxInstantShare: number, + opt?: OptionalCommonParams, +) => { + if ( + await handleRevert( + vault + .connect(opt?.from ?? owner) + .setMaxInstantShare.bind(this, maxInstantShare), + vault, + opt, ) - .withArgs((opt?.from ?? owner).address, newReceiver).to.not.reverted; + ) { + return; + } - const feeReceiver = await vault.feeReceiver(); - expect(feeReceiver).eq(newReceiver); + await expect( + vault.connect(opt?.from ?? owner).setMaxInstantShare(maxInstantShare), + ).to.emit(vault, vault.interface.events['SetMaxInstantShare(uint256)'].name); + + const newMaxInstantShare = await vault.maxInstantShare(); + expect(newMaxInstantShare).eq(maxInstantShare); }; export const setTokensReceiverTest = async ( @@ -249,42 +455,56 @@ export const setTokensReceiverTest = async ( newReceiver: string, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).setTokensReceiver(newReceiver), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + vault + .connect(opt?.from ?? owner) + .setTokensReceiver.bind(this, newReceiver), + vault, + opt, + ) + ) { return; } await expect(vault.connect(opt?.from ?? owner).setTokensReceiver(newReceiver)) - .to.emit( - vault, - vault.interface.events['SetTokensReceiver(address,address)'].name, - ) - .withArgs((opt?.from ?? owner).address, newReceiver).to.not.reverted; + .to.emit(vault, vault.interface.events['SetTokensReceiver(address)'].name) + .withArgs(newReceiver); - const feeReceiver = await vault.tokensReceiver(); - expect(feeReceiver).eq(newReceiver); + const tokensReceiver = await vault.tokensReceiver(); + expect(tokensReceiver).eq(newReceiver); }; -export const addAccountWaivedFeeRestrictionTest = async ( +export const setSequentialRequestProcessingTest = async ( { vault, owner }: CommonParamsChangePaymentToken, - account: string, + value: boolean, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).addWaivedFeeAccount(account), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + vault + .connect(opt?.from ?? owner) + .setSequentialRequestProcessing.bind(this, value), + vault, + opt, + ) + ) { return; } - await expect(vault.connect(opt?.from ?? owner).addWaivedFeeAccount(account)) + await expect( + vault.connect(opt?.from ?? owner).setSequentialRequestProcessing(value), + ) .to.emit( vault, - vault.interface.events['AddWaivedFeeAccount(address,address)'].name, + vault.interface.events['SetSequentialRequestProcessing(bool)'].name, ) - .withArgs(account, (opt?.from ?? owner).address).to.not.reverted; + .withArgs(value); + + const newSequentialRequestProcessing = + await vault.sequentialRequestProcessing(); + + expect(newSequentialRequestProcessing).eq(value); }; export const setMinAmountToDepositTest = async ( @@ -294,12 +514,15 @@ export const setMinAmountToDepositTest = async ( ) => { const value = parseUnits(valueN.toString()); - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( depositVault .connect(opt?.from ?? owner) - .setMinMTokenAmountForFirstDeposit(value), - ).revertedWith(opt?.revertMessage); + .setMinMTokenAmountForFirstDeposit.bind(this, value), + depositVault, + opt, + ) + ) { return; } @@ -309,10 +532,9 @@ export const setMinAmountToDepositTest = async ( .setMinMTokenAmountForFirstDeposit(value), ).to.emit( depositVault, - depositVault.interface.events[ - 'SetMinMTokenAmountForFirstDeposit(address,uint256)' - ].name, - ).to.not.reverted; + depositVault.interface.events['SetMinMTokenAmountForFirstDeposit(uint256)'] + .name, + ); const newMin = await depositVault.minMTokenAmountForFirstDeposit(); expect(newMin).eq(value); @@ -325,17 +547,20 @@ export const setMinAmountTest = async ( ) => { const value = parseUnits(valueN.toString()); - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).setMinAmount(value), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + vault.connect(opt?.from ?? owner).setMinAmount.bind(this, value), + vault, + opt, + ) + ) { return; } await expect(vault.connect(opt?.from ?? owner).setMinAmount(value)).to.emit( vault, - vault.interface.events['SetMinAmount(address,uint256)'].name, - ).to.not.reverted; + vault.interface.events['SetMinAmount(uint256)'].name, + ); const newMin = await vault.minAmount(); expect(newMin).eq(value); @@ -352,12 +577,15 @@ export const addPaymentTokenTest = async ( ) => { token = (token as ERC20).address ?? (token as string); - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( vault .connect(opt?.from ?? owner) - .addPaymentToken(token, dataFeed, fee, allowance, isStable), - ).revertedWith(opt?.revertMessage); + .addPaymentToken.bind(this, token, dataFeed, fee, allowance, isStable), + vault, + opt, + ) + ) { return; } @@ -368,9 +596,9 @@ export const addPaymentTokenTest = async ( ).to.emit( vault, vault.interface.events[ - 'AddPaymentToken(address,address,address,uint256,uint256,bool)' + 'AddPaymentToken(address,address,uint256,uint256,bool)' ].name, - ).to.not.reverted; + ); const paymentTokens = await vault.getPaymentTokens(); expect(paymentTokens.find((v) => v === token)).not.eq(undefined); @@ -387,55 +615,55 @@ export const removePaymentTokenTest = async ( ) => { token = (token as ERC20).address ?? (token as string); - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).removePaymentToken(token), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + vault.connect(opt?.from ?? owner).removePaymentToken.bind(this, token), + vault, + opt, + ) + ) { return; } await expect( vault.connect(opt?.from ?? owner).removePaymentToken(token), - ).to.emit( - vault, - vault.interface.events['RemovePaymentToken(address,address)'].name, - ).to.not.reverted; + ).to.emit(vault, vault.interface.events['RemovePaymentToken(address)'].name); const paymentTokens = await vault.getPaymentTokens(); expect(paymentTokens.find((v) => v === token)).eq(undefined); }; export const withdrawTest = async ( - { vault, owner }: CommonParamsChangePaymentToken, + { vault, owner }: { vault: ManageableVault; owner: SignerWithAddress }, token: IERC20 | ERC20 | string, amount: BigNumberish, - withdrawTo: Account, opt?: OptionalCommonParams, ) => { - withdrawTo = getAccount(withdrawTo); token = getAccount(token); const tokenContract = ERC20__factory.connect(token, owner); - if (opt?.revertMessage) { - await expect( - vault - .connect(opt?.from ?? owner) - .withdrawToken(token, amount, withdrawTo), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + vault.connect(opt?.from ?? owner).withdrawToken.bind(this, token, amount), + vault, + opt, + ) + ) { return; } + const withdrawTo = await vault.tokensReceiver(); + const balanceBeforeContract = await tokenContract.balanceOf(vault.address); const balanceBeforeTo = await tokenContract.balanceOf(withdrawTo); await expect( - vault.connect(opt?.from ?? owner).withdrawToken(token, amount, withdrawTo), + vault.connect(opt?.from ?? owner).withdrawToken(token, amount), ).to.emit( vault, - vault.interface.events['WithdrawToken(address,address,address,uint256)'] - .name, - ).to.not.reverted; + vault.interface.events['WithdrawToken(address,address,uint256)'].name, + ); const balanceAfterContract = await tokenContract.balanceOf(vault.address); const balanceAfterTo = await tokenContract.balanceOf(withdrawTo); @@ -443,29 +671,3 @@ export const withdrawTest = async ( expect(balanceAfterContract).eq(balanceBeforeContract.sub(amount)); expect(balanceAfterTo).eq(balanceBeforeTo.add(amount)); }; - -export const setMinBuidlToRedeem = async ( - { - vault, - owner, - }: { vault: RedemptionVaultWIthBUIDL; owner: SignerWithAddress }, - value: BigNumber, - opt?: OptionalCommonParams, -) => { - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).setMinBuidlToRedeem(value), - ).revertedWith(opt?.revertMessage); - return; - } - - await expect( - vault.connect(opt?.from ?? owner).setMinBuidlToRedeem(value), - ).to.emit( - vault, - vault.interface.events['SetMinBuidlToRedeem(uint256,address)'].name, - ).to.not.reverted; - - const newMin = await vault.minBuidlToRedeem(); - expect(newMin).eq(value); -}; diff --git a/test/common/misc/acre.helpers.ts b/test/common/misc/acre.helpers.ts index 794c318f..89db7188 100644 --- a/test/common/misc/acre.helpers.ts +++ b/test/common/misc/acre.helpers.ts @@ -7,6 +7,7 @@ import { AccountOrContract, balanceOfBase18, getAccount, + handleRevert, OptionalCommonParams, } from '../../common/common.helpers'; import { acreAdapterFixture } from '../../common/fixtures'; @@ -38,12 +39,11 @@ export const acreWrapperDepositTest = async ( const mTokenRate = await fixture.mTokenToUsdDataFeed.getDataInBase18(); - if (opt?.revertMessage) { - await expect( - fixture.acreUsdcMTbillAdapter - .connect(from) - .deposit(parseUnits(amountN.toString(), 8), receiverAddress), - ).revertedWith(opt?.revertMessage); + const callFn = fixture.acreUsdcMTbillAdapter + .connect(from) + .deposit.bind(this, parseUnits(amountN.toString(), 8), receiverAddress); + + if (await handleRevert(callFn, fixture.acreUsdcMTbillAdapter, opt)) { return; } @@ -67,11 +67,7 @@ export const acreWrapperDepositTest = async ( .connect(from) .callStatic.deposit(parseUnits(amountN.toString(), 8), receiverAddress); - await expect( - fixture.acreUsdcMTbillAdapter - .connect(from) - .deposit(parseUnits(amountN.toString(), 8), receiverAddress), - ) + await expect(callFn()) .emit( fixture.acreUsdcMTbillAdapter, fixture.acreUsdcMTbillAdapter.interface.events[ @@ -131,12 +127,11 @@ export const acreWrapperRequestRedeemTest = async ( const amountBase18 = parseUnits(amountN.toFixed(18).replace(/\.?0+$/, '')); - if (opt?.revertMessage) { - await expect( - fixture.acreUsdcMTbillAdapter - .connect(from) - .requestRedeem(amountBase18, receiverAddress), - ).revertedWith(opt?.revertMessage); + const callFn = fixture.acreUsdcMTbillAdapter + .connect(from) + .requestRedeem.bind(this, amountBase18, receiverAddress); + + if (await handleRevert(callFn, fixture.acreUsdcMTbillAdapter, opt)) { return; } @@ -159,11 +154,7 @@ export const acreWrapperRequestRedeemTest = async ( .callStatic.requestRedeem(amountBase18, receiverAddress); const requestId = await fixture.redemptionVault.currentRequestId(); - await expect( - fixture.acreUsdcMTbillAdapter - .connect(from) - .requestRedeem(amountBase18, receiverAddress), - ) + await expect(callFn()) .emit( fixture.acreUsdcMTbillAdapter, fixture.acreUsdcMTbillAdapter.interface.events[ @@ -199,7 +190,7 @@ export const acreWrapperRequestRedeemTest = async ( expect(requestIdReturned).eq(requestId); expect(request.amountMToken).eq(amountBase18); - expect(request.sender).eq(receiverAddress); + expect(request.recipient).eq(receiverAddress); expect(balanceUserAfter).eq(balanceUserBefore); expect(balanceContractAfter).eq(constants.Zero); expect(balanceMTokenAfter).eq(balanceMTokenBefore.sub(amountBase18)); diff --git a/test/common/mtoken.helpers.ts b/test/common/mtoken.helpers.ts new file mode 100644 index 00000000..410f234c --- /dev/null +++ b/test/common/mtoken.helpers.ts @@ -0,0 +1,390 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { BigNumberish, constants } from 'ethers'; +import { defaultAbiCoder, solidityKeccak256 } from 'ethers/lib/utils'; + +import { + Account, + OptionalCommonParams, + getAccount, + getCurrentBlockTimestamp, + handleRevert, +} from './common.helpers'; +import { calculateWindowRateLimitCapacity } from './manageable-vault.helpers'; + +import { MToken, MTokenPermissioned } from '../../typechain-types'; + +type CommonParams = { + tokenContract: MToken | MTokenPermissioned; + owner: SignerWithAddress; +}; + +export const setMetadataTest = async ( + { tokenContract, owner }: CommonParams, + key: string, + value: string, + opt?: OptionalCommonParams, +) => { + const keyBytes32 = solidityKeccak256(['string'], [key]); + const valueBytes = defaultAbiCoder.encode(['string'], [value]); + + if ( + await handleRevert( + tokenContract + .connect(opt?.from ?? owner) + .setMetadata.bind(this, keyBytes32, valueBytes), + tokenContract, + opt, + ) + ) { + return; + } + + await tokenContract + .connect(opt?.from ?? owner) + .setMetadata(keyBytes32, valueBytes); + + expect(await tokenContract.metadata(keyBytes32)).eq(valueBytes); +}; + +export const setClawbackReceiverTest = async ( + { tokenContract, owner }: CommonParams, + newReceiver: string, + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + + if ( + await handleRevert( + tokenContract.connect(from).setClawbackReceiver.bind(this, newReceiver), + tokenContract, + opt, + ) + ) { + return; + } + + await expect(tokenContract.connect(from).setClawbackReceiver(newReceiver)) + .to.emit( + tokenContract, + tokenContract.interface.events['ClawbackReceiverSet(address)'].name, + ) + .withArgs(newReceiver); + expect(await tokenContract.clawbackReceiver()).eq(newReceiver); +}; + +export const clawbackTest = async ( + { tokenContract, owner }: CommonParams, + amount: BigNumberish, + from: Account, + opt?: OptionalCommonParams, +) => { + const fromAddr = getAccount(from); + const caller = opt?.from ?? owner; + const receiver = await tokenContract.clawbackReceiver(); + + if ( + await handleRevert( + tokenContract.connect(caller).clawback.bind(this, amount, fromAddr), + tokenContract, + opt, + ) + ) { + return; + } + + const balanceFromBefore = await tokenContract.balanceOf(fromAddr); + const balanceReceiverBefore = await tokenContract.balanceOf(receiver); + + await expect( + tokenContract.connect(caller).clawback(amount, fromAddr), + ).to.emit( + tokenContract, + tokenContract.interface.events['Transfer(address,address,uint256)'].name, + ); + + expect(await tokenContract.balanceOf(fromAddr)).eq( + balanceFromBefore.sub(amount), + ); + expect(await tokenContract.balanceOf(receiver)).eq( + balanceReceiverBefore.add(amount), + ); +}; + +export const mint = async ( + { + tokenContract, + owner, + isGoverned = false, + }: CommonParams & { isGoverned?: boolean }, + to: Account, + amount: BigNumberish, + opt?: OptionalCommonParams, +) => { + to = getAccount(to); + + const caller = opt?.from ?? owner; + + const callFn = isGoverned + ? tokenContract.connect(caller).mintGoverned.bind(this, to, amount) + : tokenContract.connect(caller).mint.bind(this, to, amount); + + if (await handleRevert(callFn, tokenContract, opt)) { + return; + } + + const balanceBefore = await tokenContract.balanceOf(to); + + const rateLimitConfigsBefore = await tokenContract.getMintRateLimitStatuses(); + + const timetsampBefore = await getCurrentBlockTimestamp(); + + await expect(callFn()).to.emit( + tokenContract, + tokenContract.interface.events['Transfer(address,address,uint256)'].name, + ); + + const rateLimitConfigsAfter = await tokenContract.getMintRateLimitStatuses(); + const timestampAfter = await getCurrentBlockTimestamp(); + + const expectedLimitsAfter = await Promise.all( + rateLimitConfigsBefore.map(async (limit) => { + const { remaining, inFlight } = calculateWindowRateLimitCapacity({ + amountInFlight: limit.inFlight, + lastUpdated: timetsampBefore, + limit: limit.limit, + window: limit.window, + now: timestampAfter, + }); + + return { + ...limit, + remaining: remaining.gte(amount) + ? remaining.sub(amount) + : constants.Zero, + inFlight: inFlight.add(amount), + }; + }), + ); + + const currentTimestamp = await getCurrentBlockTimestamp(); + + for (const [index, limit] of rateLimitConfigsBefore.entries()) { + expect(rateLimitConfigsAfter[index].inFlight).eq( + expectedLimitsAfter[index].inFlight, + ); + expect(rateLimitConfigsAfter[index].remaining).eq( + expectedLimitsAfter[index].remaining, + ); + expect(rateLimitConfigsAfter[index].lastUpdated).eq(currentTimestamp); + expect(rateLimitConfigsAfter[index].window).eq(limit.window); + expect(rateLimitConfigsAfter[index].limit).eq(limit.limit); + } + + const balanceAfter = await tokenContract.balanceOf(to); + + expect(balanceAfter.sub(balanceBefore)).eq(amount); +}; + +export const burn = async ( + { + tokenContract, + owner, + isGoverned = false, + }: CommonParams & { isGoverned?: boolean }, + from: Account, + amount: BigNumberish, + opt?: OptionalCommonParams, +) => { + from = getAccount(from); + + const caller = opt?.from ?? owner; + + const callFn = isGoverned + ? tokenContract.connect(caller).burnGoverned.bind(this, from, amount) + : tokenContract.connect(caller).burn.bind(this, from, amount); + + if (await handleRevert(callFn, tokenContract, opt)) { + return; + } + + const balanceBefore = await tokenContract.balanceOf(from); + + const rateLimitConfigsBefore = await tokenContract.getMintRateLimitStatuses(); + + await expect(callFn()).to.emit( + tokenContract, + tokenContract.interface.events['Transfer(address,address,uint256)'].name, + ); + + const rateLimitConfigsAfter = await tokenContract.getMintRateLimitStatuses(); + + for (const [index, limit] of rateLimitConfigsBefore.entries()) { + expect(rateLimitConfigsAfter[index].limit).eq(limit.limit); + expect(rateLimitConfigsAfter[index].inFlight).gte(limit.inFlight); + expect(rateLimitConfigsAfter[index].remaining).lte(limit.remaining); + expect(rateLimitConfigsAfter[index].lastUpdated).eq(limit.lastUpdated); + expect(rateLimitConfigsAfter[index].window).eq(limit.window); + } + + const balanceAfter = await tokenContract.balanceOf(from); + + expect(balanceBefore.sub(balanceAfter)).eq(amount); +}; + +export const increaseMintRateLimitTest = async ( + { tokenContract, owner }: CommonParams, + window: number, + newLimit: BigNumberish, + opt?: OptionalCommonParams, +) => { + if ( + await handleRevert( + tokenContract + .connect(opt?.from ?? owner) + .increaseMintRateLimit.bind(this, window, newLimit), + tokenContract, + opt, + ) + ) { + return; + } + + const rateLimitConfigsBefore = await tokenContract.getMintRateLimitStatuses(); + + await expect( + tokenContract.connect(owner).increaseMintRateLimit(window, newLimit), + ) + .to.emit(tokenContract, 'WindowLimitSet') + .withArgs(window, newLimit); + const currentTimestamp = await getCurrentBlockTimestamp(); + const rateLimitConfigsAfter = await tokenContract.getMintRateLimitStatuses(); + + const configBefore = rateLimitConfigsBefore.filter((limit) => + limit.window.eq(window), + )?.[0]; + const configAfter = rateLimitConfigsAfter.filter((limit) => + limit.window.eq(window), + )?.[0]; + + if (configBefore) { + const { inFlight, remaining } = calculateWindowRateLimitCapacity({ + amountInFlight: configBefore.inFlight, + lastUpdated: configBefore.lastUpdated, + limit: newLimit, + window, + now: currentTimestamp, + }); + + expect(configAfter).not.eq(undefined); + expect(configBefore).not.eq(undefined); + expect(configAfter.limit).eq(newLimit); + expect(configAfter.lastUpdated).eq(currentTimestamp); + expect(configAfter.inFlight).eq(inFlight); + expect(configAfter.remaining).eq(remaining); + } else { + expect(configAfter).not.eq(undefined); + expect(configBefore).eq(undefined); + expect(configAfter.limit).eq(newLimit); + expect(configAfter.inFlight).eq(0); + expect(configAfter.lastUpdated).eq(currentTimestamp); + expect(configAfter.remaining).eq(newLimit); + } +}; + +export const removeMintRateLimitTest = async ( + { tokenContract, owner }: CommonParams, + window: number, + opt?: OptionalCommonParams, +) => { + if ( + await handleRevert( + tokenContract + .connect(opt?.from ?? owner) + .removeMintRateLimitConfig.bind(this, window), + tokenContract, + opt, + ) + ) { + return; + } + + const rateLimitConfigsBefore = await tokenContract.getMintRateLimitStatuses(); + + await expect(tokenContract.connect(owner).removeMintRateLimitConfig(window)) + .to.emit(tokenContract, 'WindowLimitRemoved') + .withArgs(window); + const rateLimitConfigsAfter = await tokenContract.getMintRateLimitStatuses(); + + const configBefore = rateLimitConfigsBefore.filter((limit) => + limit.window.eq(window), + )?.[0]; + const configAfter = rateLimitConfigsAfter.filter((limit) => + limit.window.eq(window), + )?.[0]; + + expect(configBefore).not.eq(undefined); + expect(configAfter).eq(undefined); +}; + +export const decreaseMintRateLimitTest = async ( + { tokenContract, owner }: CommonParams, + window: number, + newLimit: BigNumberish, + opt?: OptionalCommonParams, +) => { + if ( + await handleRevert( + tokenContract + .connect(opt?.from ?? owner) + .decreaseMintRateLimit.bind(this, window, newLimit), + tokenContract, + opt, + ) + ) { + return; + } + + const rateLimitConfigsBefore = await tokenContract.getMintRateLimitStatuses(); + + await expect( + tokenContract.connect(owner).decreaseMintRateLimit(window, newLimit), + ) + .to.emit(tokenContract, 'WindowLimitSet') + .withArgs(window, newLimit); + + const currentTimestamp = await getCurrentBlockTimestamp(); + const rateLimitConfigsAfter = await tokenContract.getMintRateLimitStatuses(); + + const configBefore = rateLimitConfigsBefore.filter((limit) => + limit.window.eq(window), + )?.[0]; + + const configAfter = rateLimitConfigsAfter.filter((limit) => + limit.window.eq(window), + )?.[0]; + + if (configBefore) { + const { inFlight, remaining } = calculateWindowRateLimitCapacity({ + amountInFlight: configBefore.inFlight, + lastUpdated: configBefore.lastUpdated, + limit: newLimit, + window, + now: currentTimestamp, + }); + + expect(configAfter).not.eq(undefined); + expect(configBefore).not.eq(undefined); + expect(configAfter.limit).eq(newLimit); + expect(configAfter.lastUpdated).eq(currentTimestamp); + expect(configAfter.inFlight).eq(inFlight); + expect(configAfter.remaining).eq(remaining); + } else { + expect(configAfter).not.eq(undefined); + expect(configBefore).eq(undefined); + expect(configAfter.limit).eq(newLimit); + expect(configAfter.inFlight).eq(0); + expect(configAfter.lastUpdated).eq(currentTimestamp); + expect(configAfter.remaining).eq(newLimit); + } +}; diff --git a/test/common/post-deploy.helpers.ts b/test/common/post-deploy.helpers.ts deleted file mode 100644 index a29fee16..00000000 --- a/test/common/post-deploy.helpers.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { expect } from 'chai'; -import { BigNumberish } from 'ethers'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; - -import { keccak256 } from './common.helpers'; - -import { getAllRoles } from '../../helpers/roles'; -import { - AggregatorV3Interface, - DataFeed, - DepositVault, - MidasAccessControl, - RedemptionVault, - MTBILL, -} from '../../typechain-types'; - -type Params = { - accessControl: MidasAccessControl; - mTBILL: MTBILL; - dataFeed: DataFeed; - dataFeedMToken: DataFeed; - aggregator: AggregatorV3Interface; - depositVault: DepositVault; - aggregatorMToken: AggregatorV3Interface; - redemptionVault: RedemptionVault; - owner: SignerWithAddress; - tokensReceiver: string; - minMTokenAmountForFirstDeposit: BigNumberish; - minAmount: BigNumberish; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - execute?: (role: string, address: string) => Promise; -}; - -export const postDeploymentTest = async ( - { ethers }: HardhatRuntimeEnvironment, - { - accessControl, - depositVault, - redemptionVault, - mTBILL, - dataFeedMToken, - aggregatorMToken, - owner, - tokensReceiver, - minMTokenAmountForFirstDeposit = '0', - minAmount, - }: Params, -) => { - const roles = getAllRoles(); - - /** mTBILL tests start */ - expect(await mTBILL.name()).eq('Midas US Treasury Bill Token'); - expect(await mTBILL.symbol()).eq('mTBILL'); - expect(await mTBILL.paused()).eq(false); - - /** mTBILL tests end */ - - /** DataFeed tests start */ - - expect(await dataFeedMToken.aggregator()).eq(aggregatorMToken.address); - - /** DataFeed tests end */ - - /** DepositVault tests start */ - - expect(await depositVault.mToken()).eq(mTBILL.address); - - expect(await depositVault.tokensReceiver()).eq(tokensReceiver); - - expect(await depositVault.ONE_HUNDRED_PERCENT()).eq('10000'); - - expect(await depositVault.minMTokenAmountForFirstDeposit()).eq( - minMTokenAmountForFirstDeposit, - ); - expect(await depositVault.minAmount()).eq(minAmount); - - expect(await depositVault.vaultRole()).eq( - keccak256('DEPOSIT_VAULT_ADMIN_ROLE'), - ); - - expect(await depositVault.MANUAL_FULLFILMENT_TOKEN()).eq( - ethers.constants.AddressZero, - ); - - /** DepositVault tests end */ - - /** RedemptionVault tests start */ - - expect(await redemptionVault.mToken()).eq(mTBILL.address); - - expect(await redemptionVault.tokensReceiver()).eq(tokensReceiver); - - expect(await redemptionVault.ONE_HUNDRED_PERCENT()).eq('10000'); - - expect(await redemptionVault.vaultRole()).eq( - keccak256('REDEMPTION_VAULT_ADMIN_ROLE'), - ); - - expect(await redemptionVault.MANUAL_FULLFILMENT_TOKEN()).eq( - ethers.constants.AddressZero, - ); - - /** RedemptionVault tests end */ - - /** Owners roles tests start */ - - const { blacklisted: _, greenlisted: __, ...rolesToCheck } = roles.common; - - for (const role of Object.values(rolesToCheck)) { - expect(await accessControl.hasRole(role, owner.address)).to.eq(true); - } - - expect(await accessControl.getRoleAdmin(roles.common.blacklisted)).eq( - roles.common.blacklistedOperator, - ); - - expect(await accessControl.getRoleAdmin(roles.common.greenlisted)).eq( - roles.common.greenlistedOperator, - ); -}; diff --git a/test/common/redemption-vault-aave.helpers.ts b/test/common/redemption-vault-aave.helpers.ts index c09bdc2f..dc708fd9 100644 --- a/test/common/redemption-vault-aave.helpers.ts +++ b/test/common/redemption-vault-aave.helpers.ts @@ -1,15 +1,21 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; import { BigNumber, BigNumberish } from 'ethers'; +import { parseUnits } from 'ethers/lib/utils'; -import { AccountOrContract, OptionalCommonParams } from './common.helpers'; +import { + AccountOrContract, + handleRevert, + OptionalCommonParams, + shouldRevert, +} from './common.helpers'; import { redeemInstantTest } from './redemption-vault.helpers'; import { IERC20, RedemptionVaultWithAave, - MTBILLTest, DataFeedTest, + MToken, } from '../../typechain-types'; type CommonParamsSetAavePool = { @@ -20,7 +26,7 @@ type CommonParamsSetAavePool = { type RedemptionWithAaveParams = { redemptionVault: RedemptionVaultWithAave; owner: SignerWithAddress; - mTBILL: MTBILLTest; + mTBILL: MToken; mTokenToUsdDataFeed: DataFeedTest; usdc: IERC20; aToken: IERC20; @@ -37,10 +43,15 @@ export const setAavePoolTest = async ( pool: string, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - redemptionVault.connect(opt?.from ?? owner).setAavePool(token, pool), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + redemptionVault + .connect(opt?.from ?? owner) + .setAavePool.bind(this, token, pool), + redemptionVault, + opt, + ) + ) { return; } @@ -48,8 +59,7 @@ export const setAavePoolTest = async ( redemptionVault.connect(opt?.from ?? owner).setAavePool(token, pool), ).to.emit( redemptionVault, - redemptionVault.interface.events['SetAavePool(address,address,address)'] - .name, + redemptionVault.interface.events['SetAavePool(address,address)'].name, ).to.not.reverted; const poolAfter = await redemptionVault.aavePools(token); @@ -61,10 +71,15 @@ export const removeAavePoolTest = async ( token: string, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - redemptionVault.connect(opt?.from ?? owner).removeAavePool(token), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + redemptionVault + .connect(opt?.from ?? owner) + .removeAavePool.bind(this, token), + redemptionVault, + opt, + ) + ) { return; } @@ -72,7 +87,7 @@ export const removeAavePoolTest = async ( redemptionVault.connect(opt?.from ?? owner).removeAavePool(token), ).to.emit( redemptionVault, - redemptionVault.interface.events['RemoveAavePool(address,address)'].name, + redemptionVault.interface.events['RemoveAavePool(address)'].name, ).to.not.reverted; const poolAfter = await redemptionVault.aavePools(token); @@ -96,7 +111,7 @@ export const redeemInstantWithAaveTest = async ( customRecipient, } = params; - if (opt?.revertMessage) { + if (shouldRevert(opt)) { await redeemInstantTest( { redemptionVault, @@ -131,6 +146,13 @@ export const redeemInstantWithAaveTest = async ( waivedFee: params.waivedFee, minAmount: params.minAmount, customRecipient, + // aTokens redeem 1:1 for the underlying, so the vault's aToken balance is + // extra tokenOut liquidity available on top of its direct tokenOut balance. + additionalLiquidity: async () => + aToken.balanceOf(redemptionVault.address), + // Real aTokens rebase, accruing a few wei of interest across the redeem + // block; tolerate that so the balance assertion isn't yield-flaky. + vaultBalanceTolerance: parseUnits('0.01', 6), }, usdc, amountTBillIn, diff --git a/test/common/redemption-vault-morpho.helpers.ts b/test/common/redemption-vault-morpho.helpers.ts index a065347e..53e89963 100644 --- a/test/common/redemption-vault-morpho.helpers.ts +++ b/test/common/redemption-vault-morpho.helpers.ts @@ -1,15 +1,22 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; import { BigNumber, BigNumberish } from 'ethers'; +import { parseUnits } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; -import { AccountOrContract, OptionalCommonParams } from './common.helpers'; +import { + AccountOrContract, + handleRevert, + OptionalCommonParams, + shouldRevert, +} from './common.helpers'; import { redeemInstantTest } from './redemption-vault.helpers'; import { IERC20, RedemptionVaultWithMorpho, - MTBILLTest, DataFeedTest, + MToken, } from '../../typechain-types'; type CommonParamsSetMorphoVault = { @@ -20,7 +27,7 @@ type CommonParamsSetMorphoVault = { type RedemptionWithMorphoParams = { redemptionVault: RedemptionVaultWithMorpho; owner: SignerWithAddress; - mTBILL: MTBILLTest; + mTBILL: MToken; mTokenToUsdDataFeed: DataFeedTest; usdc: IERC20; morphoVault: IERC20; @@ -37,10 +44,15 @@ export const setMorphoVaultTest = async ( vault: string, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - redemptionVault.connect(opt?.from ?? owner).setMorphoVault(token, vault), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + redemptionVault + .connect(opt?.from ?? owner) + .setMorphoVault.bind(this, token, vault), + redemptionVault, + opt, + ) + ) { return; } @@ -48,8 +60,7 @@ export const setMorphoVaultTest = async ( redemptionVault.connect(opt?.from ?? owner).setMorphoVault(token, vault), ).to.emit( redemptionVault, - redemptionVault.interface.events['SetMorphoVault(address,address,address)'] - .name, + redemptionVault.interface.events['SetMorphoVault(address,address)'].name, ).to.not.reverted; const vaultAfter = await redemptionVault.morphoVaults(token); @@ -61,10 +72,15 @@ export const removeMorphoVaultTest = async ( token: string, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - redemptionVault.connect(opt?.from ?? owner).removeMorphoVault(token), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + redemptionVault + .connect(opt?.from ?? owner) + .removeMorphoVault.bind(this, token), + redemptionVault, + opt, + ) + ) { return; } @@ -72,7 +88,7 @@ export const removeMorphoVaultTest = async ( redemptionVault.connect(opt?.from ?? owner).removeMorphoVault(token), ).to.emit( redemptionVault, - redemptionVault.interface.events['RemoveMorphoVault(address,address)'].name, + redemptionVault.interface.events['RemoveMorphoVault(address)'].name, ).to.not.reverted; const vaultAfter = await redemptionVault.morphoVaults(token); @@ -96,7 +112,7 @@ export const redeemInstantWithMorphoTest = async ( customRecipient, } = params; - if (opt?.revertMessage) { + if (shouldRevert(opt)) { await redeemInstantTest( { redemptionVault, @@ -122,6 +138,11 @@ export const redeemInstantWithMorphoTest = async ( usdc.balanceOf(sender.address), ]); + const morphoVaultErc4626 = await ethers.getContractAt( + 'IMorphoVault', + morphoVault.address, + ); + await redeemInstantTest( { redemptionVault, @@ -131,6 +152,14 @@ export const redeemInstantWithMorphoTest = async ( waivedFee: params.waivedFee, minAmount: params.minAmount, customRecipient, + // The vault's Morpho shares are extra tokenOut liquidity: value them in + // tokenOut units via the ERC-4626 preview. + additionalLiquidity: async () => + morphoVaultErc4626.previewRedeem( + await morphoVault.balanceOf(redemptionVault.address), + ), + // Share price accrues across the redeem block; tolerate the drift. + vaultBalanceTolerance: parseUnits('0.01', 6), }, usdc, amountTBillIn, diff --git a/test/common/redemption-vault-mtoken.helpers.ts b/test/common/redemption-vault-mtoken.helpers.ts index cfe08def..397d98e6 100644 --- a/test/common/redemption-vault-mtoken.helpers.ts +++ b/test/common/redemption-vault-mtoken.helpers.ts @@ -7,12 +7,11 @@ import { AccountOrContract, OptionalCommonParams, getAccount, + handleRevert, + shouldRevert, } from './common.helpers'; import { defaultDeploy } from './fixtures'; -import { - calcExpectedTokenOutAmount, - redeemInstantTest, -} from './redemption-vault.helpers'; +import { redeemInstantTest } from './redemption-vault.helpers'; import { ERC20, @@ -23,11 +22,11 @@ import { type CommonParamsRedeem = Pick< Awaited>, | 'owner' + | 'mTokenLoan' | 'mTBILL' - | 'mFONE' | 'redemptionVaultWithMToken' + | 'mTokenLoanToUsdDataFeed' | 'mTokenToUsdDataFeed' - | 'mFoneToUsdDataFeed' >; type CommonParamsSetVault = { @@ -39,18 +38,20 @@ export const redeemInstantWithMTokenTest = async ( { redemptionVaultWithMToken, owner, + mTokenLoan, mTBILL, - mFONE, - mFoneToUsdDataFeed, + mTokenToUsdDataFeed, useMTokenSleeve, minAmount, waivedFee, customRecipient, + additionalLiquidity, }: CommonParamsRedeem & { useMTokenSleeve?: boolean; waivedFee?: boolean; minAmount?: BigNumberish; customRecipient?: AccountOrContract; + additionalLiquidity?: () => Promise; }, tokenOut: ERC20 | string, amountMFoneIn: number, @@ -64,16 +65,17 @@ export const redeemInstantWithMTokenTest = async ( const amountIn = parseUnits(amountMFoneIn.toString()); - if (opt?.revertMessage) { + if (shouldRevert(opt)) { await redeemInstantTest( { redemptionVault: redemptionVaultWithMToken, owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, + mTBILL, + mTokenToUsdDataFeed, waivedFee, minAmount, customRecipient, + additionalLiquidity, }, tokenOut, amountMFoneIn, @@ -83,56 +85,46 @@ export const redeemInstantWithMTokenTest = async ( return; } - const balanceBeforeUserMFone = await mFONE.balanceOf(sender.address); - const balanceBeforeVaultMTbill = await mTBILL.balanceOf( + const balanceBeforeUserMFone = await mTBILL.balanceOf(sender.address); + const balanceBeforeVaultMTbill = await mTokenLoan.balanceOf( redemptionVaultWithMToken.address, ); - const supplyBeforeMFone = await mFONE.totalSupply(); - const supplyBeforeMTbill = await mTBILL.totalSupply(); - - const mFoneRate = await mFoneToUsdDataFeed.getDataInBase18(); - - const { amountInWithoutFee } = await calcExpectedTokenOutAmount( - sender, - tokenContract, - redemptionVaultWithMToken, - mFoneRate, - amountIn, - true, - ); + const supplyBeforeMFone = await mTBILL.totalSupply(); + const supplyBeforeMTbill = await mTokenLoan.totalSupply(); await redeemInstantTest( { redemptionVault: redemptionVaultWithMToken, owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, + mTBILL, + mTokenToUsdDataFeed, waivedFee, minAmount, customRecipient, + additionalLiquidity, }, tokenOut, amountMFoneIn, opt, ); - const balanceAfterUserMFone = await mFONE.balanceOf(sender.address); - const balanceAfterVaultMTbill = await mTBILL.balanceOf( + const balanceAfterUserMFone = await mTBILL.balanceOf(sender.address); + const balanceAfterVaultMTbill = await mTokenLoan.balanceOf( redemptionVaultWithMToken.address, ); - const supplyAfterMFone = await mFONE.totalSupply(); - const supplyAfterMTbill = await mTBILL.totalSupply(); + const supplyAfterMFone = await mTBILL.totalSupply(); + const supplyAfterMTbill = await mTokenLoan.totalSupply(); - // mFONE is always burned from user + // mTBILL is always burned from user expect(balanceAfterUserMFone).eq(balanceBeforeUserMFone.sub(amountIn)); - expect(supplyAfterMFone).eq(supplyBeforeMFone.sub(amountInWithoutFee)); + expect(supplyAfterMFone).eq(supplyBeforeMFone.sub(amountIn)); if (useMTokenSleeve) { - // mTBILL was redeemed from the vault's holdings + // mTokenLoan was redeemed from the vault's holdings expect(balanceAfterVaultMTbill).lt(balanceBeforeVaultMTbill); expect(supplyAfterMTbill).lt(supplyBeforeMTbill); } else { - // Vault had enough tokenOut, mTBILL untouched + // Vault had enough tokenOut, mTokenLoan untouched expect(balanceAfterVaultMTbill).eq(balanceBeforeVaultMTbill); expect(supplyAfterMTbill).eq(supplyBeforeMTbill); } @@ -143,18 +135,18 @@ export const setRedemptionVaultTest = async ( newVault: string, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).setRedemptionVault(newVault), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + vault.connect(opt?.from ?? owner).setRedemptionVault.bind(this, newVault), + vault, + opt, + ) + ) { return; } await expect(vault.connect(opt?.from ?? owner).setRedemptionVault(newVault)) - .to.emit( - vault, - vault.interface.events['SetRedemptionVault(address,address)'].name, - ) + .to.emit(vault, vault.interface.events['SetRedemptionVault(address)'].name) .withArgs((opt?.from ?? owner).address, newVault).to.not.reverted; const provider = await vault.redemptionVault(); diff --git a/test/common/redemption-vault-swapper.helpers.ts b/test/common/redemption-vault-swapper.helpers.ts deleted file mode 100644 index d749c379..00000000 --- a/test/common/redemption-vault-swapper.helpers.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { expect } from 'chai'; -import { BigNumberish } from 'ethers'; -import { parseUnits } from 'ethers/lib/utils'; - -import { - AccountOrContract, - OptionalCommonParams, - getAccount, -} from './common.helpers'; -import { defaultDeploy } from './fixtures'; -import { - calcExpectedTokenOutAmount, - redeemInstantTest, -} from './redemption-vault.helpers'; - -import { - ERC20, - ERC20__factory, - RedemptionVaultWithSwapper, -} from '../../typechain-types'; - -type CommonParamsRedeem = Pick< - Awaited>, - | 'owner' - | 'mTBILL' - | 'mBASIS' - | 'redemptionVaultWithSwapper' - | 'mTokenToUsdDataFeed' - | 'mBasisToUsdDataFeed' ->; -type CommonParamsProvider = { - vault: RedemptionVaultWithSwapper; - owner: SignerWithAddress; -}; - -export const redeemInstantWithSwapperTest = async ( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - swap, - minAmount, - waivedFee, - customRecipient, - }: CommonParamsRedeem & { - swap?: boolean; - waivedFee?: boolean; - minAmount?: BigNumberish; - customRecipient?: AccountOrContract; - }, - tokenOut: ERC20 | string, - amountTBillIn: number, - opt?: OptionalCommonParams, -) => { - tokenOut = getAccount(tokenOut); - - const tokenContract = ERC20__factory.connect(tokenOut, owner); - - const sender = opt?.from ?? owner; - - const amountIn = parseUnits(amountTBillIn.toString()); - const tokensReceiver = await redemptionVaultWithSwapper.tokensReceiver(); - const feeReceiver = await redemptionVaultWithSwapper.feeReceiver(); - const liquidityProvider = - await redemptionVaultWithSwapper.liquidityProvider(); - - if (opt?.revertMessage) { - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithSwapper, - owner, - mTBILL: mBASIS, - mTokenToUsdDataFeed: mBasisToUsdDataFeed, - waivedFee, - minAmount, - customRecipient, - checkSupply: !swap, - }, - tokenOut, - amountTBillIn, - opt, - ); - - return; - } - - const balanceBeforeUserMTBILL = await mTBILL.balanceOf(sender.address); - const balanceBeforeUserMBASIS = await mBASIS.balanceOf(sender.address); - - const balanceBeforeContractMTBILL = await mTBILL.balanceOf( - redemptionVaultWithSwapper.address, - ); - const balanceBeforeContractMBASIS = await mBASIS.balanceOf( - redemptionVaultWithSwapper.address, - ); - - const balanceBeforeProviderMTBILL = await mTBILL.balanceOf(liquidityProvider); - const balanceBeforeProviderMBASIS = await mBASIS.balanceOf(liquidityProvider); - - const balanceBeforeReceiverMTBILL = await mTBILL.balanceOf(tokensReceiver); - - const balanceBeforeFeeReceiverMTBILL = await mTBILL.balanceOf(feeReceiver); - - const supplyBeforeMTBILL = await mTBILL.totalSupply(); - const supplyBeforeMBASIS = await mBASIS.totalSupply(); - - const mBasisRate = await mBasisToUsdDataFeed.getDataInBase18(); - const mTokenRate = await mTokenToUsdDataFeed.getDataInBase18(); - - const { amountInWithoutFee } = await calcExpectedTokenOutAmount( - sender, - tokenContract, - redemptionVaultWithSwapper, - mBasisRate, - amountIn, - true, - ); - - const expectedMToken = amountInWithoutFee.mul(mBasisRate).div(mTokenRate); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithSwapper, - owner, - mTBILL: mBASIS, - mTokenToUsdDataFeed: mBasisToUsdDataFeed, - waivedFee, - minAmount, - customRecipient, - checkSupply: !swap, - }, - tokenOut, - amountTBillIn, - opt, - ); - - const balanceAfterUserMTBILL = await mTBILL.balanceOf(sender.address); - const balanceAfterUserMBASIS = await mBASIS.balanceOf(sender.address); - - const balanceAfterContractMTBILL = await mTBILL.balanceOf( - redemptionVaultWithSwapper.address, - ); - const balanceAfterContractMBASIS = await mBASIS.balanceOf( - redemptionVaultWithSwapper.address, - ); - - const balanceAfterProviderMTBILL = await mTBILL.balanceOf(liquidityProvider); - const balanceAfterProviderMBASIS = await mBASIS.balanceOf(liquidityProvider); - - const balanceAfterReceiverMTBILL = await mTBILL.balanceOf(tokensReceiver); - - const balanceAfterFeeReceiverMTBILL = await mTBILL.balanceOf(feeReceiver); - - const supplyAfterMTBILL = await mTBILL.totalSupply(); - const supplyAfterMBASIS = await mBASIS.totalSupply(); - - expect(balanceAfterUserMBASIS).eq(balanceBeforeUserMBASIS.sub(amountIn)); - expect(balanceAfterUserMTBILL).eq(balanceBeforeUserMTBILL); - - expect(balanceAfterReceiverMTBILL).eq(balanceBeforeReceiverMTBILL); - - expect(balanceAfterContractMTBILL).eq(balanceBeforeContractMTBILL); - expect(balanceAfterContractMBASIS).eq(balanceBeforeContractMBASIS); - - expect(balanceAfterFeeReceiverMTBILL).eq(balanceBeforeFeeReceiverMTBILL); - - if (swap) { - expect(supplyAfterMTBILL).eq(supplyBeforeMTBILL.sub(expectedMToken)); - expect(balanceAfterProviderMBASIS).eq( - balanceBeforeProviderMBASIS.add(amountInWithoutFee), - ); - expect(balanceAfterProviderMTBILL).eq( - balanceBeforeProviderMTBILL.sub(expectedMToken), - ); - } else { - expect(supplyAfterMBASIS).eq(supplyBeforeMBASIS.sub(amountInWithoutFee)); - expect(balanceAfterProviderMBASIS).eq(balanceBeforeProviderMBASIS); - expect(balanceAfterProviderMTBILL).eq(balanceBeforeProviderMTBILL); - } -}; - -export const setLiquidityProviderTest = async ( - { vault, owner }: CommonParamsProvider, - newProvider: string, - opt?: OptionalCommonParams, -) => { - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).setLiquidityProvider(newProvider), - ).revertedWith(opt?.revertMessage); - return; - } - - await expect( - vault.connect(opt?.from ?? owner).setLiquidityProvider(newProvider), - ) - .to.emit( - vault, - vault.interface.events['SetLiquidityProvider(address,address)'].name, - ) - .withArgs((opt?.from ?? owner).address, newProvider).to.not.reverted; - - const provider = await vault.liquidityProvider(); - expect(provider).eq(newProvider); -}; - -export const setSwapperVaultTest = async ( - { vault, owner }: CommonParamsProvider, - newVault: string, - opt?: OptionalCommonParams, -) => { - if (opt?.revertMessage) { - await expect( - vault.connect(opt?.from ?? owner).setSwapperVault(newVault), - ).revertedWith(opt?.revertMessage); - return; - } - - await expect(vault.connect(opt?.from ?? owner).setSwapperVault(newVault)) - .to.emit( - vault, - vault.interface.events['SetSwapperVault(address,address)'].name, - ) - .withArgs((opt?.from ?? owner).address, newVault).to.not.reverted; - - const provider = await vault.mTbillRedemptionVault(); - expect(provider).eq(newVault); -}; diff --git a/test/common/redemption-vault-ustb.helpers.ts b/test/common/redemption-vault-ustb.helpers.ts index c58325e6..e12cbd51 100644 --- a/test/common/redemption-vault-ustb.helpers.ts +++ b/test/common/redemption-vault-ustb.helpers.ts @@ -1,21 +1,27 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; import { BigNumber, BigNumberish } from 'ethers'; +import { parseUnits } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; -import { AccountOrContract, OptionalCommonParams } from './common.helpers'; +import { + AccountOrContract, + OptionalCommonParams, + shouldRevert, +} from './common.helpers'; import { redeemInstantTest } from './redemption-vault.helpers'; import { IERC20, RedemptionVaultWithUSTB, - MTBILLTest, DataFeedTest, + MToken, } from '../../typechain-types'; type RedemptionWithUSTBParams = { redemptionVault: RedemptionVaultWithUSTB; owner: SignerWithAddress; - mTBILL: MTBILLTest; + mTBILL: MToken; mTokenToUsdDataFeed: DataFeedTest; usdc: IERC20; ustbToken: IERC20; @@ -43,7 +49,7 @@ export const redeemInstantWithUstbTest = async ( customRecipient, } = params; - if (opt?.revertMessage) { + if (shouldRevert(opt)) { await redeemInstantTest( { redemptionVault, @@ -68,6 +74,26 @@ export const redeemInstantWithUstbTest = async ( usdc.balanceOf(sender.address), ]); + const ustbRedemption = await ethers.getContractAt( + 'IUSTBRedemption', + await redemptionVault.ustbRedemption(), + ); + // Value the vault's USTB in tokenOut (USDC) units using the redemption + // contract's own linear price, probed via calculateUstbIn. A large probe + // keeps the price ratio precise enough that valuing a whole USTB position + // doesn't accumulate meaningful rounding. + const usdcProbe = parseUnits('10000000', 6); + const valueUstbInUsdc = async () => { + const ustbBalance = await ustbToken.balanceOf(redemptionVault.address); + if (ustbBalance.isZero()) { + return BigNumber.from(0); + } + const [ustbInPerUsdcProbe] = await ustbRedemption.calculateUstbIn( + usdcProbe, + ); + return ustbBalance.mul(usdcProbe).div(ustbInPerUsdcProbe); + }; + await redeemInstantTest( { redemptionVault, @@ -77,6 +103,10 @@ export const redeemInstantWithUstbTest = async ( waivedFee: params.waivedFee, minAmount: params.minAmount, customRecipient, + // The vault's USTB is extra tokenOut liquidity redeemable for USDC. + additionalLiquidity: valueUstbInUsdc, + // Absorb price-conversion rounding across the redeem block. + vaultBalanceTolerance: parseUnits('0.01', 6), }, usdc, amountTBillIn, diff --git a/test/common/redemption-vault.helpers.ts b/test/common/redemption-vault.helpers.ts index 083d8aa1..69522753 100644 --- a/test/common/redemption-vault.helpers.ts +++ b/test/common/redemption-vault.helpers.ts @@ -1,57 +1,87 @@ +import { setNextBlockTimestamp } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { BigNumber, BigNumberish, constants } from 'ethers'; -import { parseUnits } from 'ethers/lib/utils'; +import { + BigNumber, + BigNumberish, + constants, + ContractTransaction, +} from 'ethers'; +import { formatUnits, parseUnits } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; import { AccountOrContract, OptionalCommonParams, balanceOfBase18, getAccount, + getCurrentBlockTimestamp, + handleRevert, } from './common.helpers'; import { defaultDeploy } from './fixtures'; +import { calculateWindowRateLimitCapacity } from './manageable-vault.helpers'; import { DataFeedTest__factory, ERC20, ERC20__factory, IERC20, - MTBILL, MToken, RedemptionVault, - RedemptionVaultWIthBUIDL, RedemptionVaultWithAave, RedemptionVaultWithMorpho, RedemptionVaultWithMToken, - RedemptionVaultWithSwapper, RedemptionVaultWithUSTB, + RedemptionVaultTest__factory, } from '../../typechain-types'; +type RedemptionVaultType = + | RedemptionVault + | RedemptionVaultWithAave + | RedemptionVaultWithMorpho + | RedemptionVaultWithMToken + | RedemptionVaultWithUSTB; + type CommonParamsRedeem = { - mTBILL: MToken | MTBILL; + mTBILL: MToken; } & Pick< Awaited>, 'owner' | 'mTokenToUsdDataFeed' > & { - redemptionVault: - | RedemptionVault - | RedemptionVaultWIthBUIDL - | RedemptionVaultWithAave - | RedemptionVaultWithMorpho - | RedemptionVaultWithMToken - | RedemptionVaultWithUSTB - | RedemptionVaultWithSwapper; + redemptionVault: RedemptionVaultType; }; type CommonParams = Pick>, 'owner'> & { - redemptionVault: - | RedemptionVault - | RedemptionVaultWIthBUIDL - | RedemptionVaultWithAave - | RedemptionVaultWithMorpho - | RedemptionVaultWithMToken - | RedemptionVaultWithUSTB - | RedemptionVaultWithSwapper; + redemptionVault: RedemptionVaultType; +}; + +const getTotalFromInstantShare = ( + amountIn: BigNumber, + instantShare?: BigNumberish, +) => { + if (instantShare === undefined) { + return amountIn; + } + + if (BigNumber.from(instantShare).eq(constants.Zero)) { + return BigNumber.from(0); + } + + return amountIn.mul(100_00).div(instantShare); +}; + +const expectEqWithOneWeiTolerance = ( + actual: BigNumber, + expected: BigNumber, + field: string, +) => { + const diff = actual.gte(expected) + ? actual.sub(expected) + : expected.sub(actual); + expect( + diff.lte(1), + `${field} differs by more than 1 wei: actual=${actual.toString()} expected=${expected.toString()}`, + ).eq(true); }; export const redeemInstantTest = async ( @@ -65,12 +95,27 @@ export const redeemInstantTest = async ( customRecipient, checkSupply = true, expectedAmountOut, + additionalLiquidity, + vaultBalanceTolerance, + loanLiquidityExpectToFail, + holdback, }: CommonParamsRedeem & { waivedFee?: boolean; minAmount?: BigNumberish; customRecipient?: AccountOrContract; checkSupply?: boolean; expectedAmountOut?: BigNumberish; + additionalLiquidity?: () => Promise; + // Allowed absolute drift on the vault tokenOut balance assertion. Needed + // when `additionalLiquidity` points at a live yield-bearing source (e.g. a + // rebasing aToken on a mainnet fork) that accrues a few wei of interest + // across the redeem block. Defaults to exact equality. + vaultBalanceTolerance?: BigNumberish; + loanLiquidityExpectToFail?: boolean; + holdback?: { + callFunction: () => Promise; + instantShare: BigNumberish; + }; }, tokenOut: IERC20 | ERC20 | string, amountTBillIn: number, @@ -80,118 +125,237 @@ export const redeemInstantTest = async ( const tokenContract = ERC20__factory.connect(tokenOut, owner); + const loanSwapperVault = await redemptionVault.loanSwapperVault(); + const loanSwapperVaultMToken = + loanSwapperVault !== constants.AddressZero + ? ERC20__factory.connect( + await RedemptionVaultTest__factory.connect( + loanSwapperVault, + owner, + ).mToken(), + owner, + ) + : undefined; + const sender = opt?.from ?? owner; const amountIn = parseUnits(amountTBillIn.toString()); const tokensReceiver = await redemptionVault.tokensReceiver(); - const feeReceiver = await redemptionVault.feeReceiver(); const withRecipient = customRecipient !== undefined; const recipient = customRecipient ? getAccount(customRecipient) : sender.address; - const callFn = withRecipient - ? redemptionVault - .connect(sender) - ['redeemInstant(address,uint256,uint256,address)'].bind( - this, - tokenOut, - amountIn, - minAmount ?? constants.Zero, - recipient, - ) - : redemptionVault - .connect(sender) - ['redeemInstant(address,uint256,uint256)'].bind( - this, - tokenOut, - amountIn, - minAmount ?? constants.Zero, - ); - - if (opt?.revertMessage) { - await expect(callFn()).revertedWith(opt?.revertMessage); + const callFn = + holdback?.callFunction ?? + (withRecipient + ? redemptionVault + .connect(sender) + ['redeemInstant(address,uint256,uint256,address)'].bind( + this, + tokenOut, + amountIn, + minAmount ?? constants.Zero, + recipient, + ) + : redemptionVault + .connect(sender) + ['redeemInstant(address,uint256,uint256)'].bind( + this, + tokenOut, + amountIn, + minAmount ?? constants.Zero, + )); + + if (await handleRevert(callFn, redemptionVault, opt)) { return; } const balanceBeforeUser = await mTBILL.balanceOf(sender.address); const balanceBeforeReceiver = await mTBILL.balanceOf(tokensReceiver); - const balanceBeforeFeeReceiver = await mTBILL.balanceOf(feeReceiver); - + const balanceBeforeLoanLp = loanSwapperVaultMToken + ? await loanSwapperVaultMToken.balanceOf(await redemptionVault.loanLp()) + : constants.Zero; + + const supplyBeforeLoanLp = loanSwapperVaultMToken + ? await loanSwapperVaultMToken.totalSupply() + : constants.Zero; + const balanceBeforeVault = ( + await tokenContract.balanceOf(redemptionVault.address) + ).add((await additionalLiquidity?.()) ?? constants.Zero); const balanceBeforeTokenOutRecipient = await tokenContract.balanceOf( recipient, ); const balanceBeforeTokenOut = await tokenContract.balanceOf(sender.address); const supplyBefore = await mTBILL.totalSupply(); - + const lastLoanRequestIdBefore = await redemptionVault.currentLoanRequestId(); const mTokenRate = await mTokenToUsdDataFeed.getDataInBase18(); - const { fee, amountOut, amountInWithoutFee } = - await calcExpectedTokenOutAmount( - sender, - tokenContract, - redemptionVault, - mTokenRate, - amountIn, - true, - ); + const { + fee, + amountOut, + amountOutWithoutFee, + amountOutWithoutFeeBase18, + feeBase18, + tokenOutRate, + } = await calcExpectedTokenOutAmount( + sender, + tokenContract, + redemptionVault, + mTokenRate, + amountIn, + true, + ); - await expect(callFn()) + const { + toTransferFromVault, + toTransferFromLpBase18, + toTransferFromLpMToken, + lpFeePortionBase18, + vaultFeePortion, + } = await estimateSendTokensFromLiquidity( + redemptionVault, + tokenContract, + amountOutWithoutFeeBase18!, + feeBase18!, + tokenOutRate, + { + additionalLiquidity: await additionalLiquidity?.(), + loanLiquidityExpectToFail, + }, + ); + + const instantLimitsBefore = await redemptionVault.getInstantLimitStatuses(); + const timestampBefore = await getCurrentBlockTimestamp(); + + const callPromise = callFn(); + await expect(callPromise) .to.emit( redemptionVault, redemptionVault.interface.events[ - withRecipient - ? 'RedeemInstantWithCustomRecipient(address,address,address,uint256,uint256,uint256)' - : 'RedeemInstant(address,address,uint256,uint256,uint256)' + 'RedeemInstant(address,address,address,uint256,uint256,uint256,uint256,uint256)' ].name, ) .withArgs( - ...[ - sender, - tokenOut, - withRecipient ? recipient : undefined, - amountTBillIn, - fee, - amountOut, - ].filter((v) => v !== undefined), + sender, + tokenOut, + recipient, + amountTBillIn, + feeBase18, + amountOutWithoutFeeBase18, + mTokenRate, + tokenOutRate, ).to.not.reverted; + const instantLimitsAfter = await redemptionVault.getInstantLimitStatuses(); + const timestampAfter = await getCurrentBlockTimestamp(); + + const expectedLimitsAfter = await Promise.all( + instantLimitsBefore.map(async (limit) => { + const { remaining, inFlight } = calculateWindowRateLimitCapacity({ + amountInFlight: limit.inFlight, + lastUpdated: timestampBefore, + limit: limit.limit, + window: limit.window, + now: timestampAfter, + }); + + return { + ...limit, + remaining: remaining.gte(amountIn) + ? remaining.sub(amountIn) + : constants.Zero, + inFlight: inFlight.add(amountIn), + }; + }), + ); + const balanceAfterUser = await mTBILL.balanceOf(sender.address); const balanceAfterReceiver = await mTBILL.balanceOf(tokensReceiver); - const balanceAfterFeeReceiver = await mTBILL.balanceOf(feeReceiver); + const balanceAfterLoanLp = loanSwapperVaultMToken + ? await loanSwapperVaultMToken.balanceOf(await redemptionVault.loanLp()) + : constants.Zero; + const supplyAfterLoanLp = loanSwapperVaultMToken + ? await loanSwapperVaultMToken.totalSupply() + : constants.Zero; const balanceAfterTokenOutRecipient = await tokenContract.balanceOf( recipient, ); + const balanceAfterVault = ( + await tokenContract.balanceOf(redemptionVault.address) + ).add((await additionalLiquidity?.()) ?? constants.Zero); const balanceAfterTokenOut = await tokenContract.balanceOf(sender.address); const supplyAfter = await mTBILL.totalSupply(); + const lastLoanRequestIdAfter = await redemptionVault.currentLoanRequestId(); if (checkSupply) { - expect(supplyAfter).eq(supplyBefore.sub(amountInWithoutFee)); + expect(supplyAfter).eq(supplyBefore.sub(amountIn)); } - expect(balanceAfterReceiver).eq( - balanceBeforeReceiver.add( - tokensReceiver === feeReceiver ? fee : constants.Zero, + expect(balanceAfterReceiver).eq(balanceBeforeReceiver); + expect(balanceAfterUser).eq( + balanceBeforeUser.sub( + getTotalFromInstantShare(amountIn, holdback?.instantShare), ), ); - expect(balanceAfterFeeReceiver).eq(balanceBeforeFeeReceiver.add(fee)); - - expect(balanceAfterUser).eq(balanceBeforeUser.sub(amountIn)); - - const expectedAmountToReceive = expectedAmountOut ?? amountOut; + const expectedBalanceAfterVault = balanceBeforeVault.sub(toTransferFromVault); + if (vaultBalanceTolerance !== undefined) { + expect(balanceAfterVault).closeTo( + expectedBalanceAfterVault, + vaultBalanceTolerance, + ); + } else { + expect(balanceAfterVault).eq(expectedBalanceAfterVault); + } + const expectedAmountToReceive = expectedAmountOut ?? amountOutWithoutFee!; expect(balanceAfterTokenOutRecipient).eq( balanceBeforeTokenOutRecipient.add(expectedAmountToReceive), ); if (recipient !== sender.address) { expect(balanceAfterTokenOut).eq(balanceBeforeTokenOut); } - if (waivedFee) { - expect(balanceAfterFeeReceiver).eq(balanceBeforeFeeReceiver); + + if (toTransferFromLpBase18.gt(0)) { + expect(balanceAfterLoanLp).eq( + balanceBeforeLoanLp.sub(toTransferFromLpMToken), + ); + expect(supplyAfterLoanLp).eq( + supplyBeforeLoanLp.sub(toTransferFromLpMToken), + ); + + const loanRequest = await redemptionVault.loanRequests( + lastLoanRequestIdAfter.sub(1), + ); + expect(loanRequest.amountTokenOut).eq(toTransferFromLpBase18); + expect(loanRequest.amountFee).eq(lpFeePortionBase18); + expect(loanRequest.status).eq(0); + expect(loanRequest.tokenOut).eq(tokenOut); + expect(loanRequest.createdAt).eq(await getCurrentBlockTimestamp()); + } else { + expect(lastLoanRequestIdAfter).eq(lastLoanRequestIdBefore); + } + + for (const [index, limit] of instantLimitsBefore.entries()) { + expectEqWithOneWeiTolerance( + instantLimitsAfter[index].inFlight, + expectedLimitsAfter[index].inFlight, + `instantLimits[${index}].inFlight`, + ); + expectEqWithOneWeiTolerance( + instantLimitsAfter[index].remaining, + expectedLimitsAfter[index].remaining, + `instantLimits[${index}].remaining`, + ); + expect(instantLimitsAfter[index].lastUpdated).eq(timestampAfter); + expect(instantLimitsAfter[index].window).eq(limit.window); + expect(instantLimitsAfter[index].limit).eq(limit.limit); } + + return callPromise; }; export const redeemRequestTest = async ( @@ -202,9 +366,15 @@ export const redeemRequestTest = async ( mTokenToUsdDataFeed, waivedFee, customRecipient, + customRecipientInstant, + instantShare, + minReceiveAmountInstantShare, }: CommonParamsRedeem & { waivedFee?: boolean; customRecipient?: AccountOrContract; + instantShare?: BigNumberish; + minReceiveAmountInstantShare?: BigNumberish; + customRecipientInstant?: AccountOrContract; }, tokenOut: ERC20 | string, amountTBillIn: number, @@ -218,35 +388,50 @@ export const redeemRequestTest = async ( const amountIn = parseUnits(amountTBillIn.toString()); const tokensReceiver = await redemptionVault.tokensReceiver(); - const feeReceiver = await redemptionVault.feeReceiver(); const withRecipient = customRecipient !== undefined; - const recipient = customRecipient + + const recipientRequest = customRecipient ? getAccount(customRecipient) : sender.address; + const recipientInstant = + customRecipientInstant || customRecipient + ? getAccount(customRecipientInstant ?? customRecipient!) + : sender.address; - const callFn = withRecipient - ? redemptionVault - .connect(sender) - ['redeemRequest(address,uint256,address)'].bind( - this, - tokenOut, - amountIn, - recipient, - ) - : redemptionVault - .connect(sender) - ['redeemRequest(address,uint256)'].bind(this, tokenOut, amountIn); + const callFn = + instantShare !== undefined || withRecipient + ? redemptionVault + .connect(sender) + [ + 'redeemRequest(address,uint256,address,uint256,uint256,address)' + ].bind( + this, + tokenOut, + amountIn, + recipientRequest, + instantShare ?? constants.Zero, + minReceiveAmountInstantShare ?? constants.Zero, + recipientInstant, + ) + : redemptionVault + .connect(sender) + ['redeemRequest(address,uint256)'].bind(this, tokenOut, amountIn); - if (opt?.revertMessage) { - await expect(callFn()).revertedWith(opt?.revertMessage); + if (await handleRevert(callFn, redemptionVault, opt)) { return {}; } const balanceBeforeUser = await mTBILL.balanceOf(sender.address); const balanceBeforeContract = await mTBILL.balanceOf(redemptionVault.address); const balanceBeforeReceiver = await mTBILL.balanceOf(tokensReceiver); - const balanceBeforeFeeReceiver = await mTBILL.balanceOf(feeReceiver); + const balanceBeforeRequestRedeemer = await mTBILL.balanceOf( + await redemptionVault.requestRedeemer(), + ); + const balanceBeforeContractPToken = await balanceOfBase18( + tokenContract, + redemptionVault.address, + ); const balanceBeforeTokenOut = await tokenContract.balanceOf(sender.address); @@ -255,7 +440,7 @@ export const redeemRequestTest = async ( const latestRequestIdBefore = await redemptionVault.currentRequestId(); const mTokenRate = await mTokenToUsdDataFeed.getDataInBase18(); - const { fee, currentStableRate, amountInWithoutFee } = + const { currentStableRate, tokenOutRate, feePercent } = await calcExpectedTokenOutAmount( sender, tokenContract, @@ -265,346 +450,511 @@ export const redeemRequestTest = async ( false, ); - await expect(callFn()) + const amountMTokenInInstant = amountIn + .mul(instantShare ?? constants.Zero) + .div(100_00); + const amountMTokenInRequest = amountIn.sub(amountMTokenInInstant); + + const nextExpectedRequestIdToProcessBefore = + await redemptionVault.nextExpectedRequestIdToProcess(); + + let callPromise: Awaited>; + + if (amountMTokenInInstant.gt(0)) { + callPromise = await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + waivedFee, + minAmount: minReceiveAmountInstantShare ?? constants.Zero, + customRecipient: recipientInstant, + holdback: { + callFunction: callFn, + instantShare: instantShare ?? constants.Zero, + }, + }, + tokenOut, + +formatUnits(amountMTokenInInstant, 18), + { from: sender }, + ); + } + + await expect(callPromise ?? callFn()) .to.emit( redemptionVault, redemptionVault.interface.events[ - withRecipient - ? 'RedeemRequestWithCustomRecipient(uint256,address,address,address,uint256,uint256)' - : 'RedeemRequest(uint256,address,address,uint256,uint256)' + 'RedeemRequest(uint256,address,address,address,uint256,uint256,uint256,uint256,uint256)' ].name, ) .withArgs( - ...[ - latestRequestIdBefore.add(1), - sender, - tokenOut, - withRecipient ? recipient : undefined, - amountTBillIn, - fee, - ].filter((v) => v !== undefined), - ).to.not.reverted; + latestRequestIdBefore, + sender.address, + tokenOut, + recipientRequest, + amountMTokenInRequest, + amountMTokenInInstant, + feePercent, + mTokenRate, + tokenOutRate, + ); const latestRequestIdAfter = await redemptionVault.currentRequestId(); const request = await redemptionVault.redeemRequests(latestRequestIdBefore); - - expect(request.sender).eq(recipient); + const nextExpectedRequestIdToProcessAfter = + await redemptionVault.nextExpectedRequestIdToProcess(); + expect(request.recipient).eq(recipientRequest); expect(request.tokenOut).eq(tokenOut); - expect(request.amountMToken).eq(amountInWithoutFee); + expect(request.amountMToken).eq(amountMTokenInRequest); expect(request.mTokenRate).eq(mTokenRate); expect(request.tokenOutRate).eq(currentStableRate); + expect(nextExpectedRequestIdToProcessAfter).eq( + nextExpectedRequestIdToProcessBefore, + ); + + if (waivedFee) { + expect(request.feePercent).eq(feePercent).eq(constants.Zero); + } else { + expect(request.feePercent).eq(feePercent); + } const balanceAfterUser = await mTBILL.balanceOf(sender.address); const balanceAfterReceiver = await mTBILL.balanceOf(tokensReceiver); - const balanceAfterFeeReceiver = await mTBILL.balanceOf(feeReceiver); const balanceAfterContract = await mTBILL.balanceOf(redemptionVault.address); + const balanceAfterRequestRedeemer = await mTBILL.balanceOf( + await redemptionVault.requestRedeemer(), + ); + const balanceAfterContractPToken = await balanceOfBase18( + tokenContract, + redemptionVault.address, + ); const balanceAfterTokenOut = await tokenContract.balanceOf(sender.address); const supplyAfter = await mTBILL.totalSupply(); - expect(supplyAfter).eq(supplyBefore); expect(latestRequestIdAfter).eq(latestRequestIdBefore.add(1)); expect(balanceAfterUser).eq(balanceBeforeUser.sub(amountIn)); - expect(balanceAfterContract).eq( - balanceBeforeContract.add(amountInWithoutFee), - ); + expect(balanceAfterContract).eq(balanceBeforeContract); expect(balanceAfterReceiver).eq(balanceBeforeReceiver); - expect(balanceAfterFeeReceiver).eq(balanceBeforeFeeReceiver.add(fee)); - expect(balanceAfterTokenOut).eq(balanceBeforeTokenOut); - if (waivedFee) { - expect(balanceAfterFeeReceiver).eq(balanceBeforeFeeReceiver); + + // those checks is already made in redeemInstantTest + if (amountMTokenInInstant.eq(0)) { + expect(supplyAfter).eq(supplyBefore); + expect(balanceAfterTokenOut).eq(balanceBeforeTokenOut); + expect(balanceAfterContractPToken).eq(balanceBeforeContractPToken); } + expect(balanceAfterRequestRedeemer).eq( + balanceBeforeRequestRedeemer.add(amountMTokenInRequest), + ); + return { requestId: latestRequestIdBefore, rate: mTokenRate, }; }; -export const redeemFiatRequestTest = async ( +export const approveRedeemRequestTest = async ( { redemptionVault, owner, mTBILL, - mTokenToUsdDataFeed, waivedFee, - }: CommonParamsRedeem & { waivedFee?: boolean }, - amountTBillIn: number, + isAvgRate, + }: CommonParamsRedeem & { + waivedFee?: boolean; + isAvgRate?: boolean; + }, + requestId: BigNumberish, + rate: BigNumber, opt?: OptionalCommonParams, ) => { const sender = opt?.from ?? owner; - const amountIn = parseUnits(amountTBillIn.toString()); const tokensReceiver = await redemptionVault.tokensReceiver(); - const feeReceiver = await redemptionVault.feeReceiver(); - if (opt?.revertMessage) { - await expect( - redemptionVault.connect(sender).redeemFiatRequest(amountIn), - ).revertedWith(opt?.revertMessage); + const callFn = redemptionVault + .connect(sender) + .approveRequest.bind(this, requestId, rate, isAvgRate ?? false); + + if (await handleRevert(callFn, redemptionVault, opt)) { return; } + const requestDataBefore = await redemptionVault.redeemRequests(requestId); + + let actualRate = !isAvgRate + ? rate + : BigNumber.from( + expectedHoldbackPartRateFromAvg( + requestDataBefore.amountMToken, + requestDataBefore.amountMTokenInstant, + requestDataBefore.mTokenRate, + rate, + ), + ); + + if (actualRate.eq(0)) { + actualRate = rate; + } + + const tokenContract = ERC20__factory.connect( + requestDataBefore.tokenOut, + owner, + ); + const balanceBeforeUser = await mTBILL.balanceOf(sender.address); const balanceBeforeContract = await mTBILL.balanceOf(redemptionVault.address); const balanceBeforeReceiver = await mTBILL.balanceOf(tokensReceiver); - const balanceBeforeFeeReceiver = await mTBILL.balanceOf(feeReceiver); - - const supplyBefore = await mTBILL.totalSupply(); + const balanceBeforeRequestRedeemerMToken = await mTBILL.balanceOf( + await redemptionVault.requestRedeemer(), + ); + const balanceBeforeRequestRedeemerPToken = await balanceOfBase18( + requestDataBefore.tokenOut, + await redemptionVault.requestRedeemer(), + ); - const latestRequestIdBefore = await redemptionVault.currentRequestId(); - const manualToken = await redemptionVault.MANUAL_FULLFILMENT_TOKEN(); - const fiatAdditionalFee = await redemptionVault.fiatAdditionalFee(); - const hundredPercent = await redemptionVault.ONE_HUNDRED_PERCENT(); - const flatFee = await redemptionVault.fiatFlatFee(); + const balanceBeforeVaultPToken = await balanceOfBase18( + requestDataBefore.tokenOut, + redemptionVault.address, + ); - const mTokenRate = await mTokenToUsdDataFeed.getDataInBase18(); + const supplyBefore = await mTBILL.totalSupply(); - const feePercent = await getFeePercent( + const balanceUserTokenOutBefore = await balanceOfBase18( + tokenContract, sender.address, - manualToken, - redemptionVault, - false, - fiatAdditionalFee, ); - const fee = amountIn - .mul(feePercent) - .div(hundredPercent) - .add(waivedFee ? 0 : flatFee); - const amountInWithoutFee = amountIn.sub(fee); - await expect(redemptionVault.connect(sender).redeemFiatRequest(amountIn)) + const { amountOutWithoutFeeBase18, feeBase18 } = + await calcExpectedTokenOutAmount( + sender, + tokenContract, + redemptionVault, + actualRate, + requestDataBefore.amountMToken, + false, + ); + + const nextExpectedRequestIdToProcessBefore = + await redemptionVault.nextExpectedRequestIdToProcess(); + + await expect(callFn()) .to.emit( redemptionVault, redemptionVault.interface.events[ - 'RedeemRequest(uint256,address,address,uint256,uint256)' + 'ApproveRequest(uint256,uint256,bool,bool)' ].name, ) - .withArgs( - latestRequestIdBefore.add(1), - sender, - manualToken, - amountTBillIn, - fee, - ).to.not.reverted; + .withArgs(requestId, actualRate, false, isAvgRate).to.not.reverted; - const latestRequestIdAfter = await redemptionVault.currentRequestId(); - const request = await redemptionVault.redeemRequests(latestRequestIdBefore); + const nextExpectedRequestIdToProcessAfter = + await redemptionVault.nextExpectedRequestIdToProcess(); - expect(request.sender).eq(sender.address); - expect(request.tokenOut).eq(manualToken); - expect(request.amountMToken).eq(amountInWithoutFee); - expect(request.mTokenRate).eq(mTokenRate); - expect(request.tokenOutRate).eq(parseUnits('1')); + const requestDataAfter = await redemptionVault.redeemRequests(requestId); + + expect(requestDataAfter.approvedMTokenRate).eq(actualRate); + expect(requestDataAfter.mTokenRate).eq(requestDataBefore.mTokenRate); + + if (nextExpectedRequestIdToProcessBefore.lte(requestId)) { + expect(nextExpectedRequestIdToProcessAfter).eq( + BigNumber.from(requestId).add(1), + ); + } else { + expect(nextExpectedRequestIdToProcessAfter).eq( + nextExpectedRequestIdToProcessBefore, + ); + } const balanceAfterUser = await mTBILL.balanceOf(sender.address); const balanceAfterReceiver = await mTBILL.balanceOf(tokensReceiver); - const balanceAfterFeeReceiver = await mTBILL.balanceOf(feeReceiver); + const balanceAfterRequestRedeemerMToken = await mTBILL.balanceOf( + await redemptionVault.requestRedeemer(), + ); + const balanceAfterRequestRedeemerPToken = await balanceOfBase18( + requestDataBefore.tokenOut, + await redemptionVault.requestRedeemer(), + ); + + const balanceAfterVaultPToken = await balanceOfBase18( + requestDataBefore.tokenOut, + redemptionVault.address, + ); + const balanceAfterContract = await mTBILL.balanceOf(redemptionVault.address); + const balanceUserTokenOutAfter = await balanceOfBase18( + tokenContract, + sender.address, + ); const supplyAfter = await mTBILL.totalSupply(); - expect(supplyAfter).eq(supplyBefore); - expect(latestRequestIdAfter).eq(latestRequestIdBefore.add(1)); - expect(balanceAfterUser).eq(balanceBeforeUser.sub(amountIn)); - expect(balanceAfterContract).eq( - balanceBeforeContract.add(amountInWithoutFee), + expect(requestDataAfter.amountTokenOut).eq(amountOutWithoutFeeBase18); + expect(balanceAfterRequestRedeemerMToken).eq( + balanceBeforeRequestRedeemerMToken.sub(requestDataBefore.amountMToken), + ); + expect(balanceAfterRequestRedeemerPToken).eq( + balanceBeforeRequestRedeemerPToken.sub( + feeBase18!.add(amountOutWithoutFeeBase18!), + ), ); + + expect(requestDataAfter.status).eq(1); + + expect(balanceUserTokenOutAfter).eq( + balanceUserTokenOutBefore?.add(amountOutWithoutFeeBase18!.add(feeBase18!)), + ); + expect(balanceAfterVaultPToken).eq(balanceBeforeVaultPToken); + + expect(supplyAfter).eq(supplyBefore.sub(requestDataBefore.amountMToken)); + + expect(balanceAfterUser).eq(balanceBeforeUser); + + expect(balanceAfterContract).eq(balanceBeforeContract); + expect(balanceAfterReceiver).eq(balanceBeforeReceiver); - expect(balanceAfterFeeReceiver).eq(balanceBeforeFeeReceiver.add(fee)); - if (waivedFee) { - expect(balanceAfterFeeReceiver).eq(balanceBeforeFeeReceiver); - } }; -export const approveRedeemRequestTest = async ( +export const truncateToTokenDecimals = ( + amount: BigNumber, + decimals: number, +) => { + const precision = BigNumber.from(10).pow(18 - decimals); + + return amount.div(precision).mul(precision); +}; + +export const setLoanAprTest = async ( { redemptionVault, owner, - mTBILL, - waivedFee, - }: CommonParamsRedeem & { waivedFee?: boolean }, - requestId: BigNumberish, - newTokenRate: BigNumber, + }: { + redemptionVault: RedemptionVaultType; + owner: SignerWithAddress; + }, + loanApr: BigNumberish, opt?: OptionalCommonParams, ) => { const sender = opt?.from ?? owner; - const tokensReceiver = await redemptionVault.tokensReceiver(); - const feeReceiver = await redemptionVault.feeReceiver(); + const callFn = redemptionVault.connect(sender).setLoanApr.bind(this, loanApr); - if (opt?.revertMessage) { - await expect( - redemptionVault.connect(sender).approveRequest(requestId, newTokenRate), - ).revertedWith(opt?.revertMessage); + if (await handleRevert(callFn, redemptionVault, opt)) { return; } - const requestDataBefore = await redemptionVault.redeemRequests(requestId); + await expect(callFn()) + .to.emit( + redemptionVault, + redemptionVault.interface.events['SetLoanApr(uint256)'].name, + ) + .withArgs(loanApr).to.not.reverted; + + const newLoanApr = await redemptionVault.loanApr(); + expect(newLoanApr).eq(loanApr); +}; + +export const bulkRepayLpLoanRequestTest = async ( + { + redemptionVault, + owner, + mTBILL, + }: Omit, + requests: { id: BigNumberish }[], + opt?: OptionalCommonParams, +) => { + const sender = opt?.from ?? owner; - const manualToken = await redemptionVault.MANUAL_FULLFILMENT_TOKEN(); + const requestIds = requests.map(({ id }) => id); - let tokenContract; - if (requestDataBefore.tokenOut !== manualToken) { - tokenContract = ERC20__factory.connect(requestDataBefore.tokenOut, owner); + const callFn = redemptionVault + .connect(sender) + .bulkRepayLpLoanRequest.bind(this, requestIds); + + if (await handleRevert(callFn, redemptionVault, opt)) { + return; } - const balanceBeforeUser = await mTBILL.balanceOf(sender.address); - const balanceBeforeContract = await mTBILL.balanceOf(redemptionVault.address); - const balanceBeforeReceiver = await mTBILL.balanceOf(tokensReceiver); - const balanceBeforeFeeReceiver = await mTBILL.balanceOf(feeReceiver); + const loanRepaymentAddress = await redemptionVault.loanRepaymentAddress(); + const loanLp = await redemptionVault.loanLp(); - const supplyBefore = await mTBILL.totalSupply(); + const requestDatasBefore = await Promise.all( + requestIds.map((requestId) => redemptionVault.loanRequests(requestId)), + ); - const balanceUserTokenOutBefore = - tokenContract && (await tokenContract.balanceOf(sender.address)); + const balancesBefore = await Promise.all( + requestDatasBefore.map(({ tokenOut }) => + balanceOfBase18( + ERC20__factory.connect(tokenOut, owner), + loanRepaymentAddress, + ), + ), + ); - await expect( - redemptionVault.connect(sender).approveRequest(requestId, newTokenRate), - ) - .to.emit( - redemptionVault, - redemptionVault.interface.events['ApproveRequest(uint256,uint256)'].name, - ) - .withArgs(requestId, newTokenRate).to.not.reverted; + const balancesLpBefore = await Promise.all( + requestDatasBefore.map(({ tokenOut }) => + balanceOfBase18(ERC20__factory.connect(tokenOut, owner), loanLp), + ), + ); - const requestDataAfter = await redemptionVault.redeemRequests(requestId); + const totalSupplyBefore = await mTBILL.totalSupply(); - expect(requestDataBefore.status).not.eq(requestDataAfter.status); - expect(requestDataAfter.status).eq(1); + const expectedReceivedAmounts = await Promise.all( + requestDatasBefore.map(async (requestData) => { + return requestData.amountTokenOut; + }), + ); - const balanceAfterUser = await mTBILL.balanceOf(sender.address); - const balanceAfterReceiver = await mTBILL.balanceOf(tokensReceiver); - const balanceAfterFeeReceiver = await mTBILL.balanceOf(feeReceiver); - const balanceAfterContract = await mTBILL.balanceOf(redemptionVault.address); - const balanceUserTokenOutAfter = - tokenContract && (await tokenContract.balanceOf(sender.address)); + const groupedDataBefore = requests.map(({ id }, index) => { + return { + id, + request: requestDatasBefore[index], + expectedReceivedAmount: expectedReceivedAmounts[index], + expectedReceivedFeeAmount: BigNumber.from(0), + balance: balancesBefore[index], + balanceLp: balancesLpBefore[index], + }; + }); + const expectedTotalBurned = BigNumber.from(0); - const supplyAfter = await mTBILL.totalSupply(); + const txPromise = callFn(); + await expect(txPromise).to.not.reverted; - if (requestDataBefore.tokenOut !== manualToken) { - const tokenDecimals = !tokenContract ? 18 : await tokenContract.decimals(); + const txReceipt = await (await txPromise).wait(); + const txBlock = await ethers.provider.getBlock(txReceipt.blockNumber); + const currentTimestamp = txBlock.timestamp; - const amountOut = requestDataBefore.amountMToken - .mul(newTokenRate) - .div(requestDataBefore.tokenOutRate) - .div(10 ** (18 - tokenDecimals)); + const loanApr = await redemptionVault.loanApr(); - expect(balanceUserTokenOutAfter).eq( - balanceUserTokenOutBefore?.add(amountOut), - ); - } - expect(supplyAfter).eq(supplyBefore.sub(requestDataBefore.amountMToken)); + const feePercents = await Promise.all( + requestDatasBefore.map(async (requestData) => { + const duration = BigNumber.from(currentTimestamp).sub( + requestData.createdAt, + ); - expect(balanceAfterUser).eq(balanceBeforeUser); + const tokenDecimals = await ERC20__factory.connect( + requestData.tokenOut, + owner, + ).decimals(); - expect(balanceAfterContract).eq( - balanceBeforeContract.sub(requestDataBefore.amountMToken), - ); + const accruedInterestRaw = requestData.amountTokenOut + .mul(loanApr) + .mul(duration) + .div(BigNumber.from(10_000).mul(365).mul(86400)); - expect(balanceAfterReceiver).eq(balanceBeforeReceiver); - expect(balanceAfterFeeReceiver).eq(balanceBeforeFeeReceiver); - if (waivedFee) { - expect(balanceAfterFeeReceiver).eq(balanceBeforeFeeReceiver); - } -}; + const accruedInterest = truncateToTokenDecimals( + accruedInterestRaw, + tokenDecimals, + ); -export const safeApproveRedeemRequestTest = async ( - { - redemptionVault, - owner, - mTBILL, - waivedFee, - }: CommonParamsRedeem & { waivedFee?: boolean }, - requestId: BigNumberish, - newTokenRate: BigNumber, - opt?: OptionalCommonParams, -) => { - const sender = opt?.from ?? owner; + const amountFee = accruedInterest.gt(requestData.amountFee) + ? accruedInterest + : requestData.amountFee; - const tokensReceiver = await redemptionVault.tokensReceiver(); - const feeReceiver = await redemptionVault.feeReceiver(); + return amountFee; + }), + ); - if (opt?.revertMessage) { - await expect( - redemptionVault - .connect(sender) - .safeApproveRequest(requestId, newTokenRate), - ).revertedWith(opt?.revertMessage); - return; + for (const [index, feePercent] of feePercents.entries()) { + groupedDataBefore[index].expectedReceivedFeeAmount = feePercent; } - const requestDataBefore = await redemptionVault.redeemRequests(requestId); + const parsedLogs = txReceipt.logs + .filter((v) => v.address === redemptionVault.address) + .map((log) => redemptionVault.interface.parseLog(log)) + .filter((v) => v.name === 'RepayLpLoanRequest') + .map((v) => v.args); - const tokenContract = ERC20__factory.connect( - requestDataBefore.tokenOut, - owner, + const requestDatasAfter = await Promise.all( + requestIds.map((requestId) => redemptionVault.loanRequests(requestId)), + ); + + const balancesAfter = await Promise.all( + requestDatasAfter.map(({ tokenOut }) => + balanceOfBase18( + ERC20__factory.connect(tokenOut, owner), + loanRepaymentAddress, + ), + ), ); - const balanceBeforeUser = await mTBILL.balanceOf(requestDataBefore.sender); - const balanceBeforeContract = await mTBILL.balanceOf(redemptionVault.address); - const balanceBeforeReceiver = await mTBILL.balanceOf(tokensReceiver); - const balanceBeforeFeeReceiver = await mTBILL.balanceOf(feeReceiver); + const balancesLpAfter = await Promise.all( + requestDatasAfter.map(({ tokenOut }) => + balanceOfBase18(ERC20__factory.connect(tokenOut, owner), loanLp), + ), + ); - const supplyBefore = await mTBILL.totalSupply(); + const totalSupplyAfter = await mTBILL.totalSupply(); - const balanceUserTokenOutBefore = await tokenContract.balanceOf( - requestDataBefore.sender, - ); + const groupedDataAfter = requests.map(({ id }, index) => { + return { + id, + request: requestDatasAfter[index], + balance: balancesAfter[index], + balanceLp: balancesLpAfter[index], + }; + }); - await expect( - redemptionVault.connect(sender).safeApproveRequest(requestId, newTokenRate), - ) - .to.emit( - redemptionVault, - redemptionVault.interface.events['SafeApproveRequest(uint256,uint256)'] - .name, - ) - .withArgs(requestId, newTokenRate).to.not.reverted; + expect(totalSupplyAfter).eq(totalSupplyBefore.sub(expectedTotalBurned)); - const requestDataAfter = await redemptionVault.redeemRequests(requestId); + for (const [i, { id, ...dataBefore }] of groupedDataBefore.entries()) { + const dataAfter = groupedDataAfter[i]; - expect(requestDataBefore.status).not.eq(requestDataAfter.status); - expect(requestDataAfter.status).eq(1); + const requestDataBefore = dataBefore.request; + const requestDataAfter = dataAfter.request; - const balanceAfterUser = await mTBILL.balanceOf(sender.address); - const balanceAfterReceiver = await mTBILL.balanceOf(tokensReceiver); - const balanceAfterFeeReceiver = await mTBILL.balanceOf(feeReceiver); - const balanceAfterContract = await mTBILL.balanceOf(redemptionVault.address); - const balanceUserTokenOutAfter = await tokenContract.balanceOf( - requestDataAfter.sender, - ); + const balanceAfter = dataAfter.balance; + const balanceLpAfter = dataAfter.balanceLp; - const supplyAfter = await mTBILL.totalSupply(); + const balanceBefore = dataBefore.balance; + const balanceLpBefore = dataBefore.balanceLp; - const tokenDecimals = await tokenContract.decimals(); + expect(requestDataAfter.amountFee).eq(dataBefore.expectedReceivedFeeAmount); + expect(requestDataAfter.tokenOut).eq(requestDataBefore.tokenOut); + expect(requestDataAfter.amountTokenOut).eq( + requestDataBefore.amountTokenOut, + ); - const amountOut = requestDataBefore.amountMToken - .mul(newTokenRate) - .div(requestDataBefore.tokenOutRate) - .div(10 ** (18 - tokenDecimals)); + const logs = parsedLogs.filter((log) => log.requestId.eq(id)); - expect(balanceUserTokenOutAfter).eq( - balanceUserTokenOutBefore?.add(amountOut), - ); - expect(supplyAfter).eq(supplyBefore.sub(requestDataBefore.amountMToken)); + const expectedReceivedAggregatedByUser = groupedDataBefore + .filter((v) => v.request.tokenOut === requestDataBefore.tokenOut) + .reduce((prev, curr) => { + return prev + .add(curr.expectedReceivedAmount) + .add(curr.expectedReceivedFeeAmount); + }, BigNumber.from(0)); - expect(balanceAfterUser).eq(balanceBeforeUser); + expect(logs.length).eq(1); + expect(requestDataAfter.createdAt).eq(requestDataBefore.createdAt); + expect(requestDataAfter.status).eq(1); + expect(balanceAfter).eq( + balanceBefore.sub(expectedReceivedAggregatedByUser), + ); - expect(balanceAfterContract).eq( - balanceBeforeContract.sub(requestDataBefore.amountMToken), - ); + expect(balanceLpAfter).eq( + balanceLpBefore.add(expectedReceivedAggregatedByUser), + ); + const log = logs[0]; - expect(balanceAfterReceiver).eq(balanceBeforeReceiver); - expect(balanceAfterFeeReceiver).eq(balanceBeforeFeeReceiver); - if (waivedFee) { - expect(balanceAfterFeeReceiver).eq(balanceBeforeFeeReceiver); + expect(log.requestId).eq(id); } }; export const safeBulkApproveRequestTest = async ( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }: CommonParamsRedeem, + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate, + feedIsGrowth = false, + }: CommonParamsRedeem & { + isAvgRate?: boolean; + feedIsGrowth?: boolean; + }, requests: { id: BigNumberish; expectedToExecute?: boolean }[], newRate?: BigNumberish | 'request-rate', opt?: OptionalCommonParams, @@ -617,37 +967,40 @@ export const safeBulkApproveRequestTest = async ( newRate && newRate !== 'request-rate' ? redemptionVault .connect(sender) - ['safeBulkApproveRequest(uint256[],uint256)'].bind( - this, - requestIds, - newRate, - ) + [ + `safeBulkApproveRequest${ + isAvgRate ? 'AvgRate' : '' + }(uint256[],uint256)` + ].bind(this, requestIds, newRate) : newRate === 'request-rate' ? redemptionVault .connect(sender) .safeBulkApproveRequestAtSavedRate.bind(this, requestIds) : redemptionVault .connect(sender) - ['safeBulkApproveRequest(uint256[])'].bind(this, requestIds); + [ + `safeBulkApproveRequest${isAvgRate ? 'AvgRate' : ''}(uint256[])` + ].bind(this, requestIds); - if (opt?.revertMessage) { - await expect(callFn()).revertedWith(opt?.revertMessage); + if (await handleRevert(callFn, redemptionVault, opt)) { return; } + await setNextBlockTimestamp((await getCurrentBlockTimestamp()) + 1); + const requestDatasBefore = await Promise.all( requestIds.map((requestId) => redemptionVault.redeemRequests(requestId)), ); const balancesBefore = await Promise.all( - requestDatasBefore.map(({ tokenOut, sender }) => - balanceOfBase18(ERC20__factory.connect(tokenOut, owner), sender), + requestDatasBefore.map(({ tokenOut, recipient }) => + balanceOfBase18(ERC20__factory.connect(tokenOut, owner), recipient), ), ); const totalSupplyBefore = await mTBILL.totalSupply(); - const tokenDecimals = await Promise.all( + const _tokenDecimals = await Promise.all( requestDatasBefore.map(({ tokenOut }) => ERC20__factory.connect(tokenOut, owner).decimals(), ), @@ -656,7 +1009,7 @@ export const safeBulkApproveRequestTest = async ( const feePercents = await Promise.all( requestDatasBefore.map((requestData) => getFeePercent( - requestData.sender, + requestData.recipient, requestData.tokenOut, redemptionVault, false, @@ -664,16 +1017,73 @@ export const safeBulkApproveRequestTest = async ( ), ); + const nextExpectedRequestIdToProcessBefore = + await redemptionVault.nextExpectedRequestIdToProcess(); + + const txPromise = callFn(); + await expect(txPromise).to.not.reverted; + const currentRate = await mTokenToUsdDataFeed.getDataInBase18(); - const newExpectedRate = - newRate === 'request-rate' ? undefined : newRate ?? currentRate; - const expectedReceivedAmounts = requestDatasBefore.map((requestData, i) => - requestData.amountMToken - .mul(newExpectedRate ?? requestData.mTokenRate) - .div(requestData.tokenOutRate) - .div(10 ** (18 - tokenDecimals[i])) - .mul(10 ** (18 - tokenDecimals[i])), + const nextExpectedRequestIdToProcessAfter = + await redemptionVault.nextExpectedRequestIdToProcess(); + + const expectedToExecuteRequests = requests.filter( + (v) => v.expectedToExecute ?? true, + ); + + const expectedHighestProcessedRequestId = expectedToExecuteRequests.sort( + (a, b) => +b.id.toString() - +a.id.toString(), + )[0]?.id; + + if ( + expectedHighestProcessedRequestId !== undefined && + nextExpectedRequestIdToProcessBefore.lte(expectedHighestProcessedRequestId) + ) { + expect(nextExpectedRequestIdToProcessAfter).eq( + BigNumber.from(expectedHighestProcessedRequestId).add(1), + ); + } else { + expect(nextExpectedRequestIdToProcessAfter).eq( + nextExpectedRequestIdToProcessBefore, + ); + } + + const newExpectedRate = ( + requestData: (typeof requestDatasBefore)[number], + ) => { + let rate = + newRate === 'request-rate' + ? requestData.mTokenRate + : newRate ?? currentRate; + + if (isAvgRate) { + const holdbackRate = expectedHoldbackPartRateFromAvg( + requestData.amountMToken, + requestData.amountMTokenInstant, + requestData.mTokenRate, + rate, + ); + rate = holdbackRate === 0n ? rate : holdbackRate; + } + return BigNumber.from(rate); + }; + + const expectedReceivedAmounts = await Promise.all( + requestDatasBefore.map(async (requestData) => { + const { amountOutWithoutFeeBase18 } = await calcExpectedTokenOutAmount( + sender, + ERC20__factory.connect(requestData.tokenOut, owner), + redemptionVault, + newExpectedRate(requestData), + requestData.amountMToken, + false, + requestData.feePercent, + requestData.tokenOutRate, + ); + + return amountOutWithoutFeeBase18!; + }), ); const groupedDataBefore = requests.map(({ id, expectedToExecute }, index) => { @@ -691,22 +1101,15 @@ export const safeBulkApproveRequestTest = async ( const expectedTotalBurned = groupedDataBefore .filter((v) => v.expectedToExecute) .reduce((prev, curr) => { - return prev.add( - curr.request.amountMToken.sub( - curr.request.amountMToken.mul(curr.feePercent).div(hundredPercent), - ), - ); + return prev.add(curr.request.amountMToken); }, BigNumber.from(0)); - const txPromise = callFn(); - await expect(txPromise).to.not.reverted; - const txReceipt = await (await txPromise).wait(); const parsedLogs = txReceipt.logs .filter((v) => v.address === redemptionVault.address) .map((log) => redemptionVault.interface.parseLog(log)) - .filter((v) => v.name === 'SafeApproveRequest') + .filter((v) => v.name === 'ApproveRequest') .map((v) => v.args); const requestDatasAfter = await Promise.all( @@ -714,8 +1117,8 @@ export const safeBulkApproveRequestTest = async ( ); const balancesAfter = await Promise.all( - requestDatasAfter.map(({ tokenOut, sender }) => - balanceOfBase18(ERC20__factory.connect(tokenOut, owner), sender), + requestDatasAfter.map(({ tokenOut, recipient }) => + balanceOfBase18(ERC20__factory.connect(tokenOut, owner), recipient), ), ); @@ -744,7 +1147,7 @@ export const safeBulkApproveRequestTest = async ( const balanceAfter = dataAfter.balance; const balanceBefore = dataBefore.balance; - expect(requestDataAfter.sender).eq(requestDataBefore.sender); + expect(requestDataAfter.recipient).eq(requestDataBefore.recipient); expect(requestDataAfter.tokenOut).eq(requestDataBefore.tokenOut); expect(requestDataAfter.amountMToken).eq(requestDataBefore.amountMToken); expect(requestDataAfter.tokenOutRate).eq(requestDataBefore.tokenOutRate); @@ -754,7 +1157,7 @@ export const safeBulkApproveRequestTest = async ( const expectedReceivedAggregatedByUser = groupedDataBefore .filter( (v) => - v.request.sender === requestDataBefore.sender && + v.request.recipient === requestDataBefore.recipient && v.request.tokenOut === requestDataBefore.tokenOut && v.expectedToExecute, ) @@ -763,23 +1166,27 @@ export const safeBulkApproveRequestTest = async ( }, BigNumber.from(0)); if (expectedToExecute) { + const expectedRate = newExpectedRate(requestDataBefore); expect(logs.length).eq(1); - expect(requestDataAfter.mTokenRate).eq( - newExpectedRate ?? requestDataBefore.mTokenRate, - ); + expect(requestDataAfter.approvedMTokenRate).eq(expectedRate); + expect(requestDataAfter.mTokenRate).eq(requestDataBefore.mTokenRate); + expect(requestDataAfter.status).eq(1); + expect(requestDataAfter.amountTokenOut).eq( + dataBefore.expectedReceivedAmount, + ); + expect(balanceAfter).eq( balanceBefore.add(expectedReceivedAggregatedByUser), ); const log = logs[0]; - expect(log.newMTokenRate).eq( - newExpectedRate ?? requestDataBefore.mTokenRate, - ); + expect(log.newMTokenRate).eq(expectedRate); expect(log.requestId).eq(id); } else { expect(logs.length).eq(0); expect(requestDataAfter.mTokenRate).eq(requestDataBefore.mTokenRate); + expect(requestDataAfter.approvedMTokenRate).eq(0); expect(requestDataAfter.status).eq(0); } } @@ -793,12 +1200,14 @@ export const rejectRedeemRequestTest = async ( const sender = opt?.from ?? owner; const tokensReceiver = await redemptionVault.tokensReceiver(); - const feeReceiver = await redemptionVault.feeReceiver(); - if (opt?.revertMessage) { - await expect( - redemptionVault.connect(sender).rejectRequest(requestId), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + redemptionVault.connect(sender).rejectRequest.bind(this, requestId), + redemptionVault, + opt, + ) + ) { return; } @@ -807,17 +1216,45 @@ export const rejectRedeemRequestTest = async ( const balanceBeforeUser = await mTBILL.balanceOf(sender.address); const balanceBeforeContract = await mTBILL.balanceOf(redemptionVault.address); const balanceBeforeReceiver = await mTBILL.balanceOf(tokensReceiver); - const balanceBeforeFeeReceiver = await mTBILL.balanceOf(feeReceiver); + + const balanceVaultBefore = await balanceOfBase18( + requestDataBefore.tokenOut, + redemptionVault.address, + ); + const balanceBeforeContractPToken = await balanceOfBase18( + requestDataBefore.tokenOut, + redemptionVault.address, + ); const supplyBefore = await mTBILL.totalSupply(); + const nextExpectedRequestIdToProcessBefore = + await redemptionVault.nextExpectedRequestIdToProcess(); + await expect(redemptionVault.connect(sender).rejectRequest(requestId)) .to.emit( redemptionVault, - redemptionVault.interface.events['RejectRequest(uint256,address)'].name, + redemptionVault.interface.events['RejectRequest(uint256)'].name, ) - .withArgs(requestId, sender).to.not.reverted; + .withArgs(requestId).to.not.reverted; + + const nextExpectedRequestIdToProcessAfter = + await redemptionVault.nextExpectedRequestIdToProcess(); + + if (nextExpectedRequestIdToProcessBefore.lte(requestId)) { + expect(nextExpectedRequestIdToProcessAfter).eq( + BigNumber.from(requestId).add(1), + ); + } else { + expect(nextExpectedRequestIdToProcessAfter).eq( + nextExpectedRequestIdToProcessBefore, + ); + } + const balanceVaultAfter = await balanceOfBase18( + requestDataBefore.tokenOut, + redemptionVault.address, + ); const requestDataAfter = await redemptionVault.redeemRequests(requestId); expect(requestDataBefore.status).not.eq(requestDataAfter.status); @@ -825,134 +1262,271 @@ export const rejectRedeemRequestTest = async ( const balanceAfterUser = await mTBILL.balanceOf(sender.address); const balanceAfterReceiver = await mTBILL.balanceOf(tokensReceiver); - const balanceAfterFeeReceiver = await mTBILL.balanceOf(feeReceiver); const balanceAfterContract = await mTBILL.balanceOf(redemptionVault.address); + const balanceAfterContractPToken = await balanceOfBase18( + requestDataBefore.tokenOut, + redemptionVault.address, + ); const supplyAfter = await mTBILL.totalSupply(); + expect(balanceVaultAfter).eq(balanceVaultBefore); expect(supplyAfter).eq(supplyBefore); expect(balanceAfterUser).eq(balanceBeforeUser); expect(balanceAfterContract).eq(balanceBeforeContract); expect(balanceAfterReceiver).eq(balanceBeforeReceiver); - expect(balanceAfterFeeReceiver).eq(balanceBeforeFeeReceiver); + expect(balanceAfterContractPToken).eq(balanceBeforeContractPToken); +}; + +export const cancelLpLoanRequestTest = async ( + { + redemptionVault, + owner, + mTBILL, + }: Omit, + requestId: BigNumberish, + opt?: OptionalCommonParams, +) => { + const sender = opt?.from ?? owner; + + const loanLp = await redemptionVault.loanLp(); + const loanRepaymentAddress = await redemptionVault.loanRepaymentAddress(); + + if ( + await handleRevert( + redemptionVault.connect(sender).cancelLpLoanRequest.bind(this, requestId), + redemptionVault, + opt, + ) + ) { + return; + } + + const requestDataBefore = await redemptionVault.loanRequests(requestId); + + const loanToken = ERC20__factory.connect(requestDataBefore.tokenOut, owner); + + const balanceBeforeLpRepayment = await loanToken.balanceOf( + loanRepaymentAddress, + ); + const balanceBeforeLp = await loanToken.balanceOf(loanLp); + const balanceBeforeSender = await loanToken.balanceOf(sender.address); + + const supplyBefore = await mTBILL.totalSupply(); + + await expect(redemptionVault.connect(sender).cancelLpLoanRequest(requestId)) + .to.emit( + redemptionVault, + redemptionVault.interface.events['CancelLpLoanRequest(uint256)'].name, + ) + .withArgs(requestId).to.not.reverted; + + const requestDataAfter = await redemptionVault.loanRequests(requestId); + + const balanceAfterLpRepayment = await loanToken.balanceOf( + loanRepaymentAddress, + ); + const balanceAfterLp = await loanToken.balanceOf(loanLp); + const balanceAfterSender = await loanToken.balanceOf(sender.address); + + const supplyAfter = await mTBILL.totalSupply(); + + expect(requestDataAfter.amountFee).eq(requestDataAfter.amountFee); + expect(requestDataAfter.amountTokenOut).eq(requestDataAfter.amountTokenOut); + expect(requestDataAfter.status).eq(2); + + expect(supplyAfter).eq(supplyBefore); + expect(balanceAfterLpRepayment).eq(balanceBeforeLpRepayment); + expect(balanceAfterLp).eq(balanceBeforeLp); + expect(balanceAfterSender).eq(balanceBeforeSender); }; -export const setMinFiatRedeemAmountTest = async ( +export const setRequestRedeemerTest = async ( { redemptionVault, owner }: CommonParams, - valueN: number, + redeemer: string, opt?: OptionalCommonParams, ) => { - const value = parseUnits(valueN.toString()); + if ( + await handleRevert( + redemptionVault + .connect(opt?.from ?? owner) + .setRequestRedeemer.bind(this, redeemer), + redemptionVault, + opt, + ) + ) { + return; + } + + await expect( + redemptionVault.connect(opt?.from ?? owner).setRequestRedeemer(redeemer), + ).to.emit( + redemptionVault, + redemptionVault.interface.events['SetRequestRedeemer(address)'].name, + ).to.not.reverted; + + const newRedeemer = await redemptionVault.requestRedeemer(); + expect(newRedeemer).eq(redeemer); +}; - if (opt?.revertMessage) { - await expect( - redemptionVault.connect(opt?.from ?? owner).setMinFiatRedeemAmount(value), - ).revertedWith(opt?.revertMessage); +export const setLoanLpTest = async ( + { redemptionVault, owner }: CommonParams, + loanLp: string, + opt?: OptionalCommonParams, +) => { + if ( + await handleRevert( + redemptionVault.connect(opt?.from ?? owner).setLoanLp.bind(this, loanLp), + redemptionVault, + opt, + ) + ) { return; } await expect( - redemptionVault.connect(opt?.from ?? owner).setMinFiatRedeemAmount(value), + redemptionVault.connect(opt?.from ?? owner).setLoanLp(loanLp), ).to.emit( redemptionVault, - redemptionVault.interface.events['SetMinFiatRedeemAmount(address,uint256)'] - .name, + redemptionVault.interface.events['SetLoanLp(address)'].name, ).to.not.reverted; - const newMin = await redemptionVault.minFiatRedeemAmount(); - expect(newMin).eq(value); + const newLoanLp = await redemptionVault.loanLp(); + expect(newLoanLp).eq(loanLp); }; -export const setFiatAdditionalFeeTest = async ( +export const setLoanRepaymentAddressTest = async ( { redemptionVault, owner }: CommonParams, - valueN: number, + loanRepaymentAddress: string, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - redemptionVault.connect(opt?.from ?? owner).setFiatAdditionalFee(valueN), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + redemptionVault + .connect(opt?.from ?? owner) + .setLoanRepaymentAddress.bind(this, loanRepaymentAddress), + redemptionVault, + opt, + ) + ) { return; } await expect( - redemptionVault.connect(opt?.from ?? owner).setFiatAdditionalFee(valueN), + redemptionVault + .connect(opt?.from ?? owner) + .setLoanRepaymentAddress(loanRepaymentAddress), ).to.emit( redemptionVault, - redemptionVault.interface.events['SetFiatAdditionalFee(address,uint256)'] - .name, + redemptionVault.interface.events['SetLoanRepaymentAddress(address)'].name, ).to.not.reverted; - const newfee = await redemptionVault.fiatAdditionalFee(); - expect(newfee).eq(valueN); + const newLoanRepaymentAddress = await redemptionVault.loanRepaymentAddress(); + expect(newLoanRepaymentAddress).eq(loanRepaymentAddress); }; -export const setFiatFlatFeeTest = async ( +export const setLoanSwapperVaultTest = async ( { redemptionVault, owner }: CommonParams, - valueN: number, + loanSwapperVault: string, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - redemptionVault.connect(opt?.from ?? owner).setFiatFlatFee(valueN), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + redemptionVault + .connect(opt?.from ?? owner) + .setLoanSwapperVault.bind(this, loanSwapperVault), + redemptionVault, + opt, + ) + ) { return; } await expect( - redemptionVault.connect(opt?.from ?? owner).setFiatFlatFee(valueN), + redemptionVault + .connect(opt?.from ?? owner) + .setLoanSwapperVault(loanSwapperVault), ).to.emit( redemptionVault, - redemptionVault.interface.events['SetFiatFlatFee(address,uint256)'].name, + redemptionVault.interface.events['SetLoanSwapperVault(address)'].name, ).to.not.reverted; - const newfee = await redemptionVault.fiatFlatFee(); - expect(newfee).eq(valueN); + const newLoanSwapperVault = await redemptionVault.loanSwapperVault(); + expect(newLoanSwapperVault).eq(loanSwapperVault); }; -export const setRequestRedeemerTest = async ( +export const setMaxApproveRequestIdTest = async ( { redemptionVault, owner }: CommonParams, - redeemer: string, + maxApproveRequestId: number, opt?: OptionalCommonParams, ) => { - if (opt?.revertMessage) { - await expect( - redemptionVault.connect(opt?.from ?? owner).setRequestRedeemer(redeemer), - ).revertedWith(opt?.revertMessage); + if ( + await handleRevert( + redemptionVault + .connect(opt?.from ?? owner) + .setMaxApproveRequestId.bind(this, maxApproveRequestId), + redemptionVault, + opt, + ) + ) { return; } await expect( - redemptionVault.connect(opt?.from ?? owner).setRequestRedeemer(redeemer), + redemptionVault + .connect(opt?.from ?? owner) + .setMaxApproveRequestId(maxApproveRequestId), ).to.emit( redemptionVault, - redemptionVault.interface.events['SetRequestRedeemer(address,address)'] - .name, + redemptionVault.interface.events['SetMaxApproveRequestId(uint256)'].name, ).to.not.reverted; - const newRedeemer = await redemptionVault.requestRedeemer(); - expect(newRedeemer).eq(redeemer); + const newMaxApproveRequestId = await redemptionVault.maxApproveRequestId(); + expect(newMaxApproveRequestId).eq(maxApproveRequestId); +}; + +export const setPreferLoanLiquidityTest = async ( + { redemptionVault, owner }: CommonParams, + preferLoanLiquidity: boolean, + opt?: OptionalCommonParams, +) => { + if ( + await handleRevert( + redemptionVault + .connect(opt?.from ?? owner) + .setPreferLoanLiquidity.bind(this, preferLoanLiquidity), + redemptionVault, + opt, + ) + ) { + return; + } + + await expect( + redemptionVault + .connect(opt?.from ?? owner) + .setPreferLoanLiquidity(preferLoanLiquidity), + ).to.emit( + redemptionVault, + redemptionVault.interface.events['SetPreferLoanLiquidity(bool)'].name, + ).to.not.reverted; + + const newPreferLoanLiquidity = await redemptionVault.preferLoanLiquidity(); + expect(newPreferLoanLiquidity).eq(preferLoanLiquidity); }; export const getFeePercent = async ( sender: string, token: string, - redemptionVault: - | RedemptionVault - | RedemptionVaultWIthBUIDL - | RedemptionVaultWithAave - | RedemptionVaultWithMorpho - | RedemptionVaultWithMToken - | RedemptionVaultWithSwapper - | RedemptionVaultWithUSTB, + redemptionVault: RedemptionVaultType, isInstant: boolean, - additionalFee?: BigNumber, + overrideTokenFee?: BigNumber, ) => { const tokenConfig = await redemptionVault.tokensConfig(token); let feePercent = constants.Zero; const isWaived = await redemptionVault.waivedFeeRestriction(sender); if (!isWaived) { - feePercent = additionalFee ?? tokenConfig.fee; + feePercent = overrideTokenFee ?? tokenConfig.fee; if (isInstant) { const instantFee = await redemptionVault.instantFee(); feePercent = feePercent.add(instantFee); @@ -964,17 +1538,12 @@ export const getFeePercent = async ( export const calcExpectedTokenOutAmount = async ( sender: SignerWithAddress, token: ERC20, - redemptionVault: - | RedemptionVault - | RedemptionVaultWIthBUIDL - | RedemptionVaultWithAave - | RedemptionVaultWithMorpho - | RedemptionVaultWithMToken - | RedemptionVaultWithSwapper - | RedemptionVaultWithUSTB, + redemptionVault: RedemptionVaultType, mTokenRate: BigNumber, amountIn: BigNumber, isInstant: boolean, + overrideTokenFee?: BigNumber, + overrideTokenOutRate?: BigNumber, ) => { const tokenConfig = await redemptionVault.tokensConfig(token.address); @@ -982,40 +1551,280 @@ export const calcExpectedTokenOutAmount = async ( tokenConfig.dataFeed, sender, ); - const currentTokenInRate = tokenConfig.stable - ? constants.WeiPerEther - : await dataFeedContract.getDataInBase18(); - if (currentTokenInRate.isZero()) + const currentTokenOutRate = + overrideTokenOutRate ?? + (tokenConfig.stable + ? constants.WeiPerEther + : await dataFeedContract.getDataInBase18()); + if (currentTokenOutRate.isZero()) return { amountOut: constants.Zero, amountInWithoutFee: constants.Zero, fee: constants.Zero, currentStableRate: constants.Zero, + tokenOutRate: constants.Zero, + feePercent: constants.Zero, }; - const feePercent = await getFeePercent( - sender.address, - token.address, - redemptionVault, - isInstant, - ); - - const hundredPercent = await redemptionVault.ONE_HUNDRED_PERCENT(); - const fee = amountIn.mul(feePercent).div(hundredPercent); - - const amountInWithoutFee = amountIn.sub(fee); - const tokenDecimals = await token.decimals(); - const amountOut = amountInWithoutFee + const amountOut = amountIn .mul(mTokenRate) - .div(currentTokenInRate) + .div(currentTokenOutRate) .div(10 ** (18 - tokenDecimals)); + const feePercent = + overrideTokenFee ?? + (await getFeePercent( + sender.address, + token.address, + redemptionVault, + isInstant, + )); + + const hundredPercent = await redemptionVault.ONE_HUNDRED_PERCENT(); + const fee = amountOut.mul(feePercent).div(hundredPercent); + + const amountOutWithoutFee = amountOut.sub(fee); + return { amountOut, - amountInWithoutFee, + amountOutWithoutFee: amountOutWithoutFee, fee, - currentStableRate: currentTokenInRate, + feePercent, + currentStableRate: currentTokenOutRate, + amountOutBase18: amountOut.mul(10 ** (18 - tokenDecimals)), + amountOutWithoutFeeBase18: amountOutWithoutFee.mul( + 10 ** (18 - tokenDecimals), + ), + feeBase18: fee.mul(10 ** (18 - tokenDecimals)), + tokenOutRate: currentTokenOutRate, + }; +}; + +export const estimateSendTokensFromLiquidity = async ( + redemptionVault: RedemptionVaultType, + tokenOut: ERC20, + amountTokenOutWithoutFeeBase18: BigNumber, + feeAmountBase18: BigNumber, + tokenOutRate: BigNumber, + { + additionalLiquidity, + loanLiquidityExpectToFail, + }: { + additionalLiquidity?: BigNumberish; + loanLiquidityExpectToFail?: boolean; + }, +) => { + const decimals = await tokenOut.decimals(); + const precision = BigNumber.from(10).pow(18 - decimals); + const balanceVaultBase18 = (await tokenOut.balanceOf(redemptionVault.address)) + .add(additionalLiquidity ?? constants.Zero) + .mul(precision); + + const totalAmountBase18 = amountTokenOutWithoutFeeBase18.add(feeAmountBase18); + + if (totalAmountBase18.eq(0)) { + return { + toTransferFromVaultBase18: constants.Zero, + toTransferFromLpBase18: constants.Zero, + lpFeePortionBase18: constants.Zero, + vaultFeePortionBase18: constants.Zero, + toUseVaultLiquidityBase18: constants.Zero, + toUseLpLiquidityBase18: constants.Zero, + toTransferFromVault: constants.Zero, + toTransferFromLpMToken: constants.Zero, + lpFeePortion: constants.Zero, + vaultFeePortion: constants.Zero, + toUseVaultLiquidity: constants.Zero, + toUseLpLiquidity: constants.Zero, + }; + } + + const loanSwapperVaultAddress = await redemptionVault.loanSwapperVault(); + const loanSwapperVault = + loanSwapperVaultAddress !== constants.AddressZero + ? RedemptionVaultTest__factory.connect( + loanSwapperVaultAddress, + redemptionVault.provider, + ) + : undefined; + const loanSwapperVaultMTokenDataFeed = loanSwapperVault + ? DataFeedTest__factory.connect( + await loanSwapperVault.mTokenDataFeed(), + redemptionVault.provider, + ) + : undefined; + const loanSwapperVaultMToken = loanSwapperVault + ? ERC20__factory.connect( + await loanSwapperVault.mToken(), + redemptionVault.provider, + ) + : undefined; + + const truncateToTokenDecimals = (amount: BigNumber) => + amount.div(precision).mul(precision); + + const estimateUseLoanLpLiquidity = async ( + amountTokenOutBase18: BigNumber, + totalAmount: BigNumber, + totalFee: BigNumber, + ) => { + if (amountTokenOutBase18.eq(0)) { + return { + amountReceivedBase18: constants.Zero, + feePortionBase18: constants.Zero, + }; + } + + const loanLp = await redemptionVault.loanLp(); + if ( + !loanSwapperVaultMTokenDataFeed || + !loanSwapperVaultMToken || + loanLp === constants.AddressZero + ) { + return { + amountReceivedBase18: constants.Zero, + feePortionBase18: constants.Zero, + }; + } + + const mTokenARate = await loanSwapperVaultMTokenDataFeed.getDataInBase18(); + if (mTokenARate.eq(0)) { + return { + amountReceivedBase18: constants.Zero, + feePortionBase18: constants.Zero, + }; + } + + let grossTokenOutAmount = (await loanSwapperVaultMToken.balanceOf(loanLp)) + .mul(mTokenARate) + .div(tokenOutRate); + + if (grossTokenOutAmount.gt(amountTokenOutBase18)) { + grossTokenOutAmount = amountTokenOutBase18; + } + + if (grossTokenOutAmount.eq(0)) { + return { + amountReceivedBase18: constants.Zero, + feePortionBase18: constants.Zero, + }; + } + + const feePortionBase18 = truncateToTokenDecimals( + totalFee.mul(grossTokenOutAmount).div(totalAmount), + ); + + if (grossTokenOutAmount.eq(feePortionBase18)) { + return { + amountReceivedBase18: constants.Zero, + feePortionBase18, + }; + } + + return { + amountReceivedBase18: grossTokenOutAmount.sub(feePortionBase18), + feePortionBase18, + }; + }; + + const preferLoanLiquidity = await redemptionVault.preferLoanLiquidity(); + let usedLpLiquidityBase18 = constants.Zero; + let lpFeePortionBase18 = constants.Zero; + + if (preferLoanLiquidity) { + ({ + amountReceivedBase18: usedLpLiquidityBase18, + feePortionBase18: lpFeePortionBase18, + } = loanLiquidityExpectToFail + ? { + amountReceivedBase18: constants.Zero, + feePortionBase18: constants.Zero, + } + : await estimateUseLoanLpLiquidity( + totalAmountBase18, + totalAmountBase18, + feeAmountBase18, + )); + } else { + const obtainedVaultLiquidityBase18 = constants.Zero; + const newBalanceBase18 = balanceVaultBase18.add( + obtainedVaultLiquidityBase18, + ); + + if (newBalanceBase18.lt(totalAmountBase18)) { + ({ + amountReceivedBase18: usedLpLiquidityBase18, + feePortionBase18: lpFeePortionBase18, + } = await estimateUseLoanLpLiquidity( + totalAmountBase18.sub(newBalanceBase18), + totalAmountBase18, + feeAmountBase18, + )); + } + } + + const toTransferFromLpBase18 = usedLpLiquidityBase18; + const toTransferFromVaultBase18 = amountTokenOutWithoutFeeBase18.gte( + toTransferFromLpBase18, + ) + ? amountTokenOutWithoutFeeBase18.sub(toTransferFromLpBase18) + : constants.Zero; + const vaultFeePortionBase18 = feeAmountBase18.sub(lpFeePortionBase18); + const toUseLpLiquidityBase18 = toTransferFromLpBase18.add(lpFeePortionBase18); + const toUseVaultLiquidityBase18 = totalAmountBase18.sub( + toUseLpLiquidityBase18, + ); + + const mTokenARate = loanSwapperVaultMTokenDataFeed + ? await loanSwapperVaultMTokenDataFeed.getDataInBase18() + : constants.Zero; + const mTokenAAmount = + toTransferFromLpBase18.eq(0) || mTokenARate.eq(0) + ? constants.Zero + : toTransferFromLpBase18 + .mul(tokenOutRate) + .add(mTokenARate.sub(1)) + .div(mTokenARate); + + return { + toTransferFromVaultBase18, + toTransferFromLpBase18, + lpFeePortionBase18, + vaultFeePortionBase18, + toUseVaultLiquidityBase18, + toUseLpLiquidityBase18, + toTransferFromVault: toTransferFromVaultBase18.div(precision), + toTransferFromLpMToken: mTokenAAmount, + lpFeePortion: lpFeePortionBase18.div(precision), + vaultFeePortion: vaultFeePortionBase18.div(precision), + toUseVaultLiquidity: toUseVaultLiquidityBase18.div(precision), + toUseLpLiquidity: toUseLpLiquidityBase18.div(precision), }; }; + +export const expectedHoldbackPartRateFromAvg = ( + amountMToken: BigNumberish, + amountMTokenInstant: BigNumberish, + mTokenRate: BigNumberish, + avgMTokenRate: BigNumberish, +): BigNumberish => { + amountMToken = BigNumber.from(amountMToken).toBigInt(); + amountMTokenInstant = BigNumber.from(amountMTokenInstant).toBigInt(); + mTokenRate = BigNumber.from(mTokenRate).toBigInt(); + avgMTokenRate = BigNumber.from(avgMTokenRate).toBigInt(); + + if (amountMTokenInstant === 0n) { + return 0n; + } + + const targetTotalValue = + ((amountMToken + amountMTokenInstant) * avgMTokenRate) / 10n ** 18n; + const instantPartValue = (amountMTokenInstant * mTokenRate) / 10n ** 18n; + if (targetTotalValue <= instantPartValue) { + return 0n; + } + const holdbackPartValue = targetTotalValue - instantPartValue; + return (holdbackPartValue * 10n ** 18n) / amountMToken; +}; diff --git a/test/common/timelock-manager.helpers.ts b/test/common/timelock-manager.helpers.ts new file mode 100644 index 00000000..3437b8bc --- /dev/null +++ b/test/common/timelock-manager.helpers.ts @@ -0,0 +1,630 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { BigNumber, BigNumberish, ethers } from 'ethers'; + +import { + OptionalCommonParams, + asyncForEach, + getCurrentBlockTimestamp, + handleRevert, +} from './common.helpers'; + +import { + MidasAccessControl, + MidasAccessControlTimelockController, + MidasTimelockManager, +} from '../../typechain-types'; + +type CommonParamsTimelock = { + timelockManager: MidasTimelockManager; + accessControl: MidasAccessControl; + timelock: MidasAccessControlTimelockController; + owner: SignerWithAddress; +}; + +export const setMaxPendingOperationsPerProposerTester = async ( + { timelockManager, owner }: CommonParamsTimelock, + maxPendingOperationsPerProposer: BigNumberish, + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + const callFn = timelockManager + .connect(from) + .setMaxPendingOperationsPerProposer.bind( + this, + maxPendingOperationsPerProposer, + ); + + if (await handleRevert(callFn, timelockManager, opt)) { + return; + } + + await expect(callFn()) + .to.emit( + timelockManager, + timelockManager.interface.events[ + 'SetMaxPendingOperationsPerProposer(uint256)' + ].name, + ) + .withArgs(maxPendingOperationsPerProposer); + + const actualMaxPendingOperationsPerProposer = + await timelockManager.maxPendingOperationsPerProposer(); + + expect(actualMaxPendingOperationsPerProposer).eq( + maxPendingOperationsPerProposer, + ); +}; + +export const bulkScheduleTimelockOperationTester = async ( + { timelockManager, timelock, owner }: CommonParamsTimelock, + target: string[], + data: string[], + { isSetCouncilOperation }: { isSetCouncilOperation?: boolean } = {}, + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + isSetCouncilOperation ??= false; + + const params = target.map((target, index) => ({ + target, + data: data[index], + })); + + const callFn = timelockManager + .connect(from) + .bulkScheduleTimelockOperation.bind(this, params); + + if (await handleRevert(callFn, timelockManager, opt)) { + return []; + } + + const councilVersionBefore = await timelockManager.securityCouncilVersion(); + + const expectedOperationIds = await Promise.all( + target.map((operationTarget, index) => + getExpectedOperationId( + timelockManager, + timelock, + operationTarget, + data[index], + ), + ), + ); + + const txPromise = callFn(); + let bulkScheduleExpect = expect(txPromise); + for (const [index] of target.entries()) { + bulkScheduleExpect = bulkScheduleExpect.to + .emit( + timelockManager, + timelockManager.interface.events[ + 'ScheduleTimelockOperation(address,bytes32)' + ].name, + ) + .withArgs(from.address, expectedOperationIds[index]); + } + await bulkScheduleExpect; + const councilVersionAfter = await timelockManager.securityCouncilVersion(); + + expect(councilVersionAfter).to.be.equal(councilVersionBefore); + + const operationIds: string[] = []; + await asyncForEach(target.entries(), async ([index, operationTarget]) => { + const operationData = data[index]; + operationIds.push( + await validateOperationDetails({ + timelockManager, + timelock, + from, + operationTarget, + operationData, + isSetCouncilOperation, + councilVersionBefore, + }), + ); + }); + + return operationIds; +}; + +export const scheduleTimelockOperationTester = async ( + { timelockManager, timelock, owner }: CommonParamsTimelock, + target: string, + data: string, + { isSetCouncilOperation }: { isSetCouncilOperation?: boolean } = {}, + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + isSetCouncilOperation ??= false; + + const callFn = timelockManager + .connect(from) + .scheduleTimelockOperation.bind(this, { target, data }); + + if (await handleRevert(callFn, timelockManager, opt)) { + return []; + } + + const councilVersionBefore = await timelockManager.securityCouncilVersion(); + + const expectedOperationId = await getExpectedOperationId( + timelockManager, + timelock, + target, + data, + ); + + const txPromise = callFn(); + await expect(txPromise) + .to.emit( + timelockManager, + timelockManager.interface.events[ + 'ScheduleTimelockOperation(address,bytes32)' + ].name, + ) + .withArgs(from.address, expectedOperationId); + const councilVersionAfter = await timelockManager.securityCouncilVersion(); + + expect(councilVersionAfter).to.be.equal(councilVersionBefore); + + return [ + await validateOperationDetails({ + timelockManager, + timelock, + from, + operationTarget: target, + operationData: data, + isSetCouncilOperation, + councilVersionBefore, + }), + ]; +}; + +const getExpectedOperationId = async ( + timelockManager: MidasTimelockManager, + timelock: MidasAccessControlTimelockController, + operationTarget: string, + operationData: string, +) => { + const dataHash = getDataHash(operationTarget, operationData); + const dataHashIndex = await timelockManager.dataHashIndexes(dataHash); + + return timelock.hashOperation( + operationTarget, + 0, + operationData, + ethers.constants.HashZero, + getTimelockSalt(dataHashIndex), + ); +}; + +const validateOperationDetails = async ({ + timelockManager, + timelock, + from, + operationTarget, + operationData, + isSetCouncilOperation, + councilVersionBefore, +}: { + operationTarget: string; + operationData: string; + timelockManager: MidasTimelockManager; + timelock: MidasAccessControlTimelockController; + from: SignerWithAddress; + isSetCouncilOperation: boolean; + councilVersionBefore: BigNumber; +}) => { + const blockTimestamp = await getCurrentBlockTimestamp(); + + const dataHash = getDataHash(operationTarget, operationData); + + const dataHashIndex = await timelockManager.dataHashIndexes(dataHash); + + const operationId = await timelock.hashOperation( + operationTarget, + 0, + operationData, + ethers.constants.HashZero, + getTimelockSalt(dataHashIndex), + ); + + expect(await timelock.isOperation(operationId)).to.be.true; + expect(await timelock.isOperationReady(operationId)).to.be.false; + expect(await timelock.isOperationDone(operationId)).to.be.false; + expect(await timelock.isOperationPending(operationId)).to.be.true; + + const details = await timelockManager.getOperationDetails(operationId); + expect(details.status).to.be.equal(1); + expect(details.pauser).to.be.equal(ethers.constants.AddressZero); + expect(details.operationProposer).to.be.equal(from.address); + expect(details.dataHash).to.be.equal(dataHash); + expect(details.votesForExecution).to.be.equal(0); + expect(details.votesForVeto).to.be.equal(0); + expect(details.createdAt).to.be.equal(blockTimestamp); + expect(details.executionApprovedAt).to.be.equal(0); + expect(details.pauseReasonCode).to.be.equal(0); + expect(details.councilVersion).to.be.equal(councilVersionBefore); + expect(details.isSetCouncilOperation).to.be.equal(isSetCouncilOperation); + + return operationId; +}; + +export const executeTimelockOperationTester = async ( + { timelockManager, timelock, owner }: CommonParamsTimelock, + target: string, + data: string, + originalCaller: string, + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + + const callFn = timelockManager + .connect(from) + .executeTimelockOperation.bind(this, target, data); + + if (await handleRevert(callFn, timelockManager, opt)) { + return; + } + + const dataHash = getDataHash(target, data); + + const dataHashIndexBefore = await timelockManager.dataHashIndexes(dataHash); + + const operationId = await timelock.hashOperation( + target, + 0, + data, + ethers.constants.HashZero, + getTimelockSalt(dataHashIndexBefore), + ); + + const detailsBefore = await timelockManager.getOperationDetails(operationId); + + const txPromise = callFn(); + await expect(txPromise) + .to.emit( + timelockManager, + timelockManager.interface.events[ + 'ExecuteTimelockOperation(address,bytes32)' + ].name, + ) + .withArgs(from.address, operationId); + + const dataHashIndexAfter = await timelockManager.dataHashIndexes(dataHash); + + expect(dataHashIndexAfter).to.eq(dataHashIndexBefore.add(1)); + + expect(await timelock.isOperation(operationId)).to.be.true; + expect(await timelock.isOperationReady(operationId)).to.be.false; + expect(await timelock.isOperationDone(operationId)).to.be.true; + expect(await timelock.isOperationPending(operationId)).to.be.false; + + const detailsAfter = await timelockManager.getOperationDetails(operationId); + + expect(detailsAfter.status).to.be.equal(8); + expect(detailsAfter.pauser).to.be.equal(detailsBefore.pauser); + expect(detailsAfter.operationProposer).to.be.equal( + detailsBefore.operationProposer, + ); + expect(detailsAfter.dataHash).to.be.equal(detailsBefore.dataHash); + expect(detailsAfter.votesForExecution).to.be.equal( + detailsBefore.votesForExecution, + ); + expect(detailsAfter.votesForVeto).to.be.equal(detailsBefore.votesForVeto); + expect(detailsAfter.createdAt).to.be.equal(detailsBefore.createdAt); + expect(detailsAfter.executionApprovedAt).to.be.equal( + detailsBefore.executionApprovedAt, + ); + expect(detailsAfter.pauseReasonCode).to.be.equal( + detailsBefore.pauseReasonCode, + ); + expect(detailsAfter.councilVersion).to.be.equal(detailsBefore.councilVersion); + expect(detailsAfter.isSetCouncilOperation).to.be.equal( + detailsBefore.isSetCouncilOperation, + ); +}; + +export const setSecurityCouncilTest = async ( + { timelockManager, owner }: CommonParamsTimelock, + members: string[], + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + const callFn = timelockManager + .connect(from) + .setSecurityCouncil.bind(this, members); + + if (await handleRevert(callFn, timelockManager, opt)) { + return; + } + + const councilVersionBefore = await timelockManager.securityCouncilVersion(); + const oldCouncilBefore = await timelockManager.getSecurityCouncilMembers( + councilVersionBefore, + ); + + await expect(callFn()) + .to.emit( + timelockManager, + timelockManager.interface.events['SetSecurityCouncil(uint256,address[])'] + .name, + ) + .withArgs(councilVersionBefore.add(1), members); + const oldCouncilAfter = await timelockManager.getSecurityCouncilMembers( + councilVersionBefore, + ); + + const councilVersionAfter = await timelockManager.securityCouncilVersion(); + + expect(councilVersionAfter).to.be.equal(councilVersionBefore.add(1)); + const currentCouncilAfter = await timelockManager.getSecurityCouncilMembers( + councilVersionAfter, + ); + expect(currentCouncilAfter.length).to.be.equal(members.length); + for (const member of members) { + expect(currentCouncilAfter.includes(member)).to.be.true; + } + + // check that old council is not changed + for (const member of oldCouncilBefore) { + expect(oldCouncilAfter.includes(member)).to.be.true; + } +}; + +export const pauseTimelockOperationTest = async ( + { timelockManager, owner }: CommonParamsTimelock, + operationId: string, + pauseReasonCode: BigNumberish = 1, + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + const callFn = timelockManager + .connect(from) + .pauseOperation.bind(this, operationId, pauseReasonCode); + + if (await handleRevert(callFn, timelockManager, opt)) { + return; + } + + const detailsBefore = await timelockManager.getOperationDetails(operationId); + + await expect(callFn()) + .to.emit( + timelockManager, + timelockManager.interface.events[ + 'PauseTimelockOperation(address,bytes32,uint8,uint256)' + ].name, + ) + .withArgs( + from.address, + operationId, + pauseReasonCode, + detailsBefore.councilVersion, + ); + + const details = await timelockManager.getOperationDetails(operationId); + + expect(details.status).to.be.equal(2); + expect(details.pauser).to.be.equal(from.address); + expect(details.pauseReasonCode).to.be.equal(pauseReasonCode); + expect(details.councilVersion).to.be.equal(detailsBefore.councilVersion); + expect(details.operationProposer).to.be.equal( + detailsBefore.operationProposer, + ); + expect(details.dataHash).to.be.equal(detailsBefore.dataHash); + expect(details.votesForExecution).to.be.equal(0); + expect(details.votesForVeto).to.be.equal(0); + expect(details.isSetCouncilOperation).to.be.equal( + detailsBefore.isSetCouncilOperation, + ); + expect(details.createdAt).to.be.equal(detailsBefore.createdAt); + expect(details.executionApprovedAt).to.be.equal(0); +}; + +export const voteForVetoTest = async ( + { timelockManager, owner }: CommonParamsTimelock, + operationId: string, + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + const callFn = timelockManager + .connect(from) + .voteForVeto.bind(this, operationId); + + if (await handleRevert(callFn, timelockManager, opt)) { + return; + } + + const detailsBefore = await timelockManager.getOperationDetails(operationId); + + await expect(callFn()) + .to.emit( + timelockManager, + timelockManager.interface.events[ + 'PausedProposalVoteCast(address,bytes32,bool)' + ].name, + ) + .withArgs(from.address, operationId, false); + + const quorum = await timelockManager.councilQuorum( + detailsBefore.councilVersion, + ); + + const details = await timelockManager.getOperationDetails(operationId); + expect(details.pauser).to.be.equal(detailsBefore.pauser); + expect(details.pauseReasonCode).to.be.equal(detailsBefore.pauseReasonCode); + expect(details.councilVersion).to.be.equal(detailsBefore.councilVersion); + expect(details.operationProposer).to.be.equal( + detailsBefore.operationProposer, + ); + expect(details.dataHash).to.be.equal(detailsBefore.dataHash); + expect(details.votesForExecution).to.be.equal( + detailsBefore.votesForExecution, + ); + expect(details.votesForVeto).to.be.equal(detailsBefore.votesForVeto + 1); + + if (details.votesForVeto >= quorum) { + expect(details.status).to.be.equal(5); + } else { + expect(details.status).to.be.equal(detailsBefore.status); + } +}; + +export const voteForExecutionTest = async ( + { timelockManager, owner }: CommonParamsTimelock, + operationId: string, + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + const callFn = timelockManager + .connect(from) + .voteForExecution.bind(this, operationId); + + if (await handleRevert(callFn, timelockManager, opt)) { + return; + } + + const detailsBefore = await timelockManager.getOperationDetails(operationId); + + await expect(callFn()) + .to.emit( + timelockManager, + timelockManager.interface.events[ + 'PausedProposalVoteCast(address,bytes32,bool)' + ].name, + ) + .withArgs(from.address, operationId, true); + + const quorum = await timelockManager.councilQuorum( + detailsBefore.councilVersion, + ); + + const details = await timelockManager.getOperationDetails(operationId); + expect(details.pauser).to.be.equal(detailsBefore.pauser); + expect(details.pauseReasonCode).to.be.equal(detailsBefore.pauseReasonCode); + expect(details.councilVersion).to.be.equal(detailsBefore.councilVersion); + expect(details.operationProposer).to.be.equal( + detailsBefore.operationProposer, + ); + expect(details.dataHash).to.be.equal(detailsBefore.dataHash); + expect(details.votesForExecution).to.be.equal( + detailsBefore.votesForExecution + 1, + ); + expect(details.votesForVeto).to.be.equal(detailsBefore.votesForVeto); + + if (details.votesForExecution >= quorum) { + expect(details.status).to.be.equal(3); + expect(details.executionApprovedAt).to.be.equal( + await getCurrentBlockTimestamp(), + ); + } else { + expect(details.status).to.be.equal(detailsBefore.status); + } +}; + +export const abortOperationTest = async ( + { timelockManager, owner, timelock }: CommonParamsTimelock, + operationId: string, + opt?: OptionalCommonParams, +) => { + const from = opt?.from ?? owner; + const callFn = timelockManager + .connect(from) + .abortOperation.bind(this, operationId); + + if (await handleRevert(callFn, timelockManager, opt)) { + return; + } + + const detailsBefore = await timelockManager.getOperationDetails(operationId); + + const pendingSetCouncilOperationIdBefore = + await timelockManager.pendingSetCouncilOperationId(); + + const dataHashIndexBefore = await timelockManager.dataHashIndexes( + detailsBefore.dataHash, + ); + + const pendingOperationsBefore = await timelockManager.getPendingOperations(); + const pendingOperationsPerProposerBefore = + await timelockManager.proposerPendingOperationsCount( + detailsBefore.operationProposer, + ); + await expect(callFn()) + .to.emit( + timelockManager, + timelockManager.interface.events[ + 'AbortTimelockOperation(address,bytes32,uint8)' + ].name, + ) + .withArgs(from.address, operationId, detailsBefore.status); + + const pendingOperationsPerProposerAfter = + await timelockManager.proposerPendingOperationsCount( + detailsBefore.operationProposer, + ); + const pendingOperationsAfter = await timelockManager.getPendingOperations(); + + expect(pendingOperationsAfter.length).to.be.equal( + pendingOperationsBefore.length - 1, + ); + expect(pendingOperationsAfter.includes(operationId)).to.be.false; + + expect(pendingOperationsPerProposerAfter).to.be.equal( + pendingOperationsPerProposerBefore.sub(1), + ); + + const dataHashIndexAfter = await timelockManager.dataHashIndexes( + detailsBefore.dataHash, + ); + + expect(dataHashIndexAfter).to.be.equal(dataHashIndexBefore.add(1)); + + const pendingSetCouncilOperationIdAfter = + await timelockManager.pendingSetCouncilOperationId(); + + const details = await timelockManager.getOperationDetails(operationId); + + expect(details.status).to.be.equal(7); + expect(details.pauser).to.be.equal(detailsBefore.pauser); + expect(details.pauseReasonCode).to.be.equal(detailsBefore.pauseReasonCode); + expect(details.councilVersion).to.be.equal(detailsBefore.councilVersion); + expect(details.operationProposer).to.be.equal( + detailsBefore.operationProposer, + ); + expect(details.dataHash).to.be.equal(detailsBefore.dataHash); + expect(details.votesForExecution).to.be.equal( + detailsBefore.votesForExecution, + ); + expect(details.votesForVeto).to.be.equal(detailsBefore.votesForVeto); + expect(details.isSetCouncilOperation).to.be.equal( + detailsBefore.isSetCouncilOperation, + ); + expect(details.createdAt).to.be.equal(detailsBefore.createdAt); + expect(details.executionApprovedAt).to.be.equal( + detailsBefore.executionApprovedAt, + ); + expect(await timelock.isOperation(operationId)).to.be.false; + + if (details.isSetCouncilOperation) { + expect(pendingSetCouncilOperationIdAfter).to.be.equal( + ethers.constants.HashZero, + ); + } else { + expect(pendingSetCouncilOperationIdAfter).to.be.equal( + pendingSetCouncilOperationIdBefore, + ); + } +}; + +const getTimelockSalt = (dataHashIndex: BigNumber) => { + return ethers.utils.hexZeroPad(dataHashIndex.toHexString(), 32); +}; +const getDataHash = (target: string, data: string) => { + return ethers.utils.solidityKeccak256( + ['address', 'uint256', 'bytes'], + [target, 0, data], + ); +}; diff --git a/test/common/token.tests.ts b/test/common/token.tests.ts deleted file mode 100644 index b17aa030..00000000 --- a/test/common/token.tests.ts +++ /dev/null @@ -1,858 +0,0 @@ -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; -import { expect } from 'chai'; -import { Contract } from 'ethers'; -import { parseUnits } from 'ethers/lib/utils'; -import { ethers } from 'hardhat'; - -import { acErrors, blackList, unBlackList } from './ac.helpers'; -import { defaultDeploy } from './fixtures'; -import { burn, mint, setMetadataTest } from './mTBILL.helpers'; - -import { MTokenName } from '../../config'; -import { getTokenContractNames } from '../../helpers/contracts'; -import { mTokensMetadata } from '../../helpers/mtokens-metadata'; -import { - getAllRoles, - getRolesForToken, - getRolesNamesCommon, - getRolesNamesForToken, - tokenLevelGreenlistTokens, -} from '../../helpers/roles'; -import { - CustomAggregatorV3CompatibleFeed, - CustomAggregatorV3CompatibleFeedGrowth, - DataFeed, - DepositVault, - DepositVaultWithUSTB, - MTBILL, - RedemptionVault, - RedemptionVaultWIthBUIDL, - RedemptionVaultWithSwapper, -} from '../../typechain-types'; - -export const tokenContractsTests = (token: MTokenName) => { - const contractNames = getTokenContractNames(token); - const allRoles = getAllRoles(); - const allRoleNames = getRolesNamesCommon(); - - const tokenRoles = getRolesForToken(token); - const tokenRoleNames = getRolesNamesForToken(token); - - const isTac = token.startsWith('TAC'); - const contractNamesForTac = getTokenContractNames( - token.replace('TAC', '') as MTokenName, - ); - - const getContractFactory = async (contract: string) => { - return await ethers.getContractFactory(contract); - }; - - const deployProxyContract = async ( - contractKey: keyof Omit, - initializer = 'initialize', - ...initParams: unknown[] - ) => { - const shouldReplaceTacContracts = - isTac && - (contractKey === 'customAggregator' || contractKey === 'dataFeed'); - - const factory = await getContractFactory( - shouldReplaceTacContracts - ? contractNamesForTac[contractKey]! - : contractNames[contractKey]!, - ); - - const impl = await factory.deploy(); - - const proxy = await ( - await getContractFactory('ERC1967Proxy') - ).deploy( - impl.address, - factory.interface.encodeFunctionData(initializer, initParams), - ); - - return factory.attach(proxy.address) as TContract; - }; - - const deployProxyContractIfExists = async < - TContract extends Contract = Contract, - >( - contractKey: keyof Omit, - initializer = 'initialize', - ...initParams: unknown[] - ) => { - const shouldReplaceTacContracts = - isTac && - (contractKey === 'customAggregator' || contractKey === 'dataFeed'); - - const factory = await getContractFactory( - shouldReplaceTacContracts - ? contractNamesForTac[contractKey]! - : contractNames[contractKey]!, - ).catch((_) => { - return null; - }); - - if (!factory) { - return null; - } - - const impl = await factory.deploy(); - - const proxy = await ( - await getContractFactory('ERC1967Proxy') - ).deploy( - impl.address, - factory.interface.encodeFunctionData(initializer, initParams), - ); - - return factory.attach(proxy.address) as TContract; - }; - - const deployMTokenWithFixture = async () => { - const fixture = await loadFixture(defaultDeploy); - - const tokenContract = (await deployProxyContract( - 'token', - undefined, - fixture.accessControl.address, - )) as MTBILL; - - if (mTokensMetadata[token]?.isPermissioned) { - const greenlistedRole = tokenRoles.greenlisted; - for (const account of fixture.regularAccounts) { - await fixture.accessControl - .connect(fixture.owner) - .grantRole(greenlistedRole, account.address); - } - } - - return { tokenContract, ...fixture }; - }; - - const deployMTokenVaultsWithFixture = async () => { - const { tokenContract, ...fixture } = await deployMTokenWithFixture(); - const customAggregatorFeed = - await deployProxyContractIfExists( - 'customAggregator', - undefined, - fixture.accessControl.address, - 2, - parseUnits('10000', 8), - parseUnits('1', 8), - 'Custom Data Feed', - ); - - const customAggregatorFeedGrowth = - await deployProxyContractIfExists( - 'customAggregatorGrowth', - undefined, - fixture.accessControl.address, - 2, - parseUnits('10000', 8), - parseUnits('1', 8), - parseUnits('0', 8), - parseUnits('100', 8), - false, - 'Custom Data Feed', - ); - - await customAggregatorFeed?.setRoundData?.(parseUnits('1.01', 8)); - await customAggregatorFeedGrowth?.setRoundData?.( - parseUnits('1.01', 8), - await ethers.provider.getBlock('latest').then((block) => block.timestamp), - 0, - ); - - const dataFeed = await deployProxyContract( - 'dataFeed', - undefined, - fixture.accessControl.address, - customAggregatorFeed?.address ?? customAggregatorFeedGrowth?.address, - 3 * 24 * 3600, - parseUnits('0.1', 8), - parseUnits('10000', 8), - ); - - const depositVault = await deployProxyContractIfExists( - 'dv', - undefined, - fixture.accessControl.address, - { - mToken: tokenContract.address, - mTokenDataFeed: dataFeed.address, - }, - { - feeReceiver: fixture.feeReceiver.address, - tokensReceiver: fixture.tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - fixture.mockedSanctionsList.address, - 1, - parseUnits('100'), - 0, - 0, - ); - - const depositVaultUstb = - await deployProxyContractIfExists( - 'dvUstb', - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,uint256,uint256,address)', - fixture.accessControl.address, - { - mToken: tokenContract.address, - mTokenDataFeed: dataFeed.address, - }, - { - feeReceiver: fixture.feeReceiver.address, - tokensReceiver: fixture.tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - fixture.mockedSanctionsList.address, - 1, - parseUnits('100'), - 0, - 0, - fixture.ustbToken.address, - ); - - const redemptionVault = await deployProxyContractIfExists( - 'rv', - undefined, - fixture.accessControl.address, - { - mToken: tokenContract.address, - mTokenDataFeed: dataFeed.address, - }, - { - feeReceiver: fixture.feeReceiver.address, - tokensReceiver: fixture.tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - fixture.mockedSanctionsList.address, - 1, - 1000, - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, - }, - fixture.requestRedeemer.address, - ); - - const redemptionVaultWithSwapper = - await deployProxyContractIfExists( - 'rvSwapper', - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address,address)', - fixture.accessControl.address, - { - mToken: tokenContract.address, - mTokenDataFeed: dataFeed.address, - }, - { - feeReceiver: fixture.feeReceiver.address, - tokensReceiver: fixture.tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - fixture.mockedSanctionsList.address, - 1, - 1000, - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, - }, - fixture.requestRedeemer.address, - fixture.redemptionVault.address, - fixture.liquidityProvider.address, - ); - - await redemptionVaultWithSwapper?.addWaivedFeeAccount( - fixture.redemptionVault.address, - ); - - const redemptionVaultWithBuidl = - await deployProxyContractIfExists( - 'rvBuidl', - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address,uint256,uint256)', - fixture.accessControl.address, - { - mToken: tokenContract.address, - mTokenDataFeed: dataFeed.address, - }, - { - feeReceiver: fixture.feeReceiver.address, - tokensReceiver: fixture.tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - fixture.mockedSanctionsList.address, - 1, - 1000, - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, - }, - fixture.requestRedeemer.address, - fixture.buidlRedemption.address, - parseUnits('250000', 6), - parseUnits('250000', 6), - ); - - return { - ...fixture, - tokenContract, - tokenDataFeed: dataFeed, - tokenCustomAggregatorFeed: customAggregatorFeed, - tokenDepositVault: depositVault, - tokenDepositVaultUstb: depositVaultUstb, - tokenRedemptionVault: redemptionVault, - tokenRedemptionVaultWithSwapper: redemptionVaultWithSwapper, - tokenRedemptionVaultWithBuidl: redemptionVaultWithBuidl, - tokenCustomAggregatorFeedGrowth: customAggregatorFeedGrowth, - }; - }; - - describe(`Token`, function () { - it('deployment', async () => { - const { tokenContract } = await deployMTokenWithFixture(); - - const expected = mTokensMetadata[token]; - expect(await tokenContract.name()).eq(expected.name); - expect(await tokenContract.symbol()).eq(expected.symbol); - - expect(await tokenContract.paused()).eq(false); - }); - - it('roles', async () => { - const { tokenContract } = await deployMTokenWithFixture(); - - const contract = tokenContract as Contract; - - expect(await contract[tokenRoleNames.burner]()).eq(tokenRoles.burner); - expect(await contract[tokenRoleNames.minter]()).eq(tokenRoles.minter); - expect(await contract[tokenRoleNames.pauser]()).eq(tokenRoles.pauser); - - expect(await contract[allRoleNames.defaultAdmin]()).eq( - allRoles.common.defaultAdmin, - ); - expect(await contract[allRoleNames.blacklistedOperator]()).eq( - allRoles.common.blacklistedOperator, - ); - expect(await contract[allRoleNames.greenlistedOperator]()).eq( - allRoles.common.greenlistedOperator, - ); - }); - - it('initialize', async () => { - const { tokenContract } = await deployMTokenWithFixture(); - - await expect( - tokenContract.initialize(ethers.constants.AddressZero), - ).revertedWith('Initializable: contract is already initialized'); - }); - - describe('pause()', () => { - it('should fail: call from address without "token pauser" role', async () => { - const { accessControl, regularAccounts, tokenContract } = - await deployMTokenWithFixture(); - - const caller = regularAccounts[0]; - - await expect(tokenContract.connect(caller).pause()).revertedWith( - acErrors.WMAC_HASNT_ROLE, - ); - }); - - it('should fail: call when already paused', async () => { - const { accessControl, tokenContract, owner } = - await deployMTokenWithFixture(); - - await tokenContract.connect(owner).pause(); - await expect(tokenContract.connect(owner).pause()).revertedWith( - `Pausable: paused`, - ); - }); - - it('call when unpaused', async () => { - const { owner, tokenContract } = await deployMTokenWithFixture(); - - expect(await tokenContract.paused()).eq(false); - await expect(tokenContract.connect(owner).pause()).to.emit( - tokenContract, - tokenContract.interface.events['Paused(address)'].name, - ).to.not.reverted; - expect(await tokenContract.paused()).eq(true); - }); - }); - - describe('unpause()', () => { - it('should fail: call from address without "token pauser" role', async () => { - const { owner, tokenContract, regularAccounts } = - await deployMTokenWithFixture(); - - const caller = regularAccounts[0]; - - await tokenContract.connect(owner).pause(); - await expect(tokenContract.connect(caller).unpause()).revertedWith( - acErrors.WMAC_HASNT_ROLE, - ); - }); - - it('should fail: call when already paused', async () => { - const { owner, tokenContract } = await deployMTokenWithFixture(); - - await expect(tokenContract.connect(owner).unpause()).revertedWith( - `Pausable: not paused`, - ); - }); - - it('call when paused', async () => { - const { owner, tokenContract } = await deployMTokenWithFixture(); - - expect(await tokenContract.paused()).eq(false); - await tokenContract.connect(owner).pause(); - expect(await tokenContract.paused()).eq(true); - - await expect(tokenContract.connect(owner).unpause()).to.emit( - tokenContract, - tokenContract.interface.events['Unpaused(address)'].name, - ).to.not.reverted; - - expect(await tokenContract.paused()).eq(false); - }); - }); - - describe('mint()', () => { - it('should fail: call from address without "mint operator" role', async () => { - const { owner, tokenContract, regularAccounts } = - await deployMTokenWithFixture(); - const caller = regularAccounts[0]; - - await mint({ tokenContract, owner }, owner, 0, { - from: caller, - revertMessage: acErrors.WMAC_HASNT_ROLE, - }); - }); - - it('call from address with "mint operator" role', async () => { - const { owner, tokenContract, regularAccounts } = - await deployMTokenWithFixture(); - - const amount = parseUnits('100'); - const to = regularAccounts[0].address; - - await mint({ tokenContract, owner }, to, amount); - }); - }); - - describe('burn()', () => { - it('should fail: call from address without "burn operator" role', async () => { - const { owner, tokenContract, regularAccounts } = - await deployMTokenWithFixture(); - - const caller = regularAccounts[0]; - - await burn({ tokenContract, owner }, owner, 0, { - from: caller, - revertMessage: acErrors.WMAC_HASNT_ROLE, - }); - }); - - it('should fail: call when user has insufficient balance', async () => { - const { owner, tokenContract, regularAccounts } = - await deployMTokenWithFixture(); - - const amount = parseUnits('100'); - const to = regularAccounts[0].address; - - await burn({ tokenContract, owner }, to, amount, { - revertMessage: 'ERC20: burn amount exceeds balance', - }); - }); - - it('call from address with "mint operator" role', async () => { - const { owner, tokenContract, regularAccounts } = - await deployMTokenWithFixture(); - - const amount = parseUnits('100'); - const to = regularAccounts[0].address; - - await mint({ tokenContract, owner }, to, amount); - await burn({ tokenContract, owner }, to, amount); - }); - }); - - describe('setMetadata()', () => { - it('should fail: call from address without DEFAULT_ADMIN_ROLE role', async () => { - const { owner, tokenContract, regularAccounts } = - await deployMTokenWithFixture(); - - const caller = regularAccounts[0]; - - await setMetadataTest({ tokenContract, owner }, 'url', 'some value', { - from: caller, - revertMessage: acErrors.WMAC_HASNT_ROLE, - }); - }); - - it('call from address with DEFAULT_ADMIN_ROLE role', async () => { - const { owner, tokenContract } = await deployMTokenWithFixture(); - - await setMetadataTest( - { tokenContract, owner }, - 'url', - 'some value', - undefined, - ); - }); - }); - - describe('_beforeTokenTransfer()', () => { - it('should fail: mint(...) when address is blacklisted', async () => { - const { owner, regularAccounts, accessControl, tokenContract } = - await deployMTokenWithFixture(); - const blacklisted = regularAccounts[0]; - - await blackList( - { blacklistable: tokenContract, accessControl, owner }, - blacklisted, - ); - await mint({ tokenContract, owner }, blacklisted, 1, { - revertMessage: acErrors.WMAC_HAS_ROLE, - }); - }); - - it('should fail: transfer(...) when from address is blacklisted', async () => { - const { owner, regularAccounts, accessControl, tokenContract } = - await deployMTokenWithFixture(); - - const blacklisted = regularAccounts[0]; - const to = regularAccounts[1]; - - await mint({ tokenContract, owner }, blacklisted, 1); - await blackList( - { blacklistable: tokenContract, accessControl, owner }, - blacklisted, - ); - - await expect( - tokenContract.connect(blacklisted).transfer(to.address, 1), - ).revertedWith(acErrors.WMAC_HAS_ROLE); - }); - - it('should fail: transfer(...) when to address is blacklisted', async () => { - const { owner, regularAccounts, accessControl, tokenContract } = - await deployMTokenWithFixture(); - - const blacklisted = regularAccounts[0]; - const from = regularAccounts[1]; - - await mint({ tokenContract, owner }, from, 1); - await blackList( - { blacklistable: tokenContract, accessControl, owner }, - blacklisted, - ); - - await expect( - tokenContract.connect(from).transfer(blacklisted.address, 1), - ).revertedWith(acErrors.WMAC_HAS_ROLE); - }); - - it('should fail: transferFrom(...) when from address is blacklisted', async () => { - const { owner, regularAccounts, accessControl, tokenContract } = - await deployMTokenWithFixture(); - - const blacklisted = regularAccounts[0]; - const to = regularAccounts[1]; - - await mint({ tokenContract, owner }, blacklisted, 1); - await blackList( - { blacklistable: tokenContract, accessControl, owner }, - blacklisted, - ); - - await tokenContract.connect(blacklisted).approve(to.address, 1); - - await expect( - tokenContract - .connect(to) - .transferFrom(blacklisted.address, to.address, 1), - ).revertedWith(acErrors.WMAC_HAS_ROLE); - }); - - it('should fail: transferFrom(...) when to address is blacklisted', async () => { - const { owner, regularAccounts, accessControl, tokenContract } = - await deployMTokenWithFixture(); - - const blacklisted = regularAccounts[0]; - const from = regularAccounts[1]; - const caller = regularAccounts[2]; - - await mint({ tokenContract, owner }, from, 1); - - await blackList( - { blacklistable: tokenContract, accessControl, owner }, - blacklisted, - ); - await tokenContract.connect(from).approve(caller.address, 1); - - await expect( - tokenContract - .connect(caller) - .transferFrom(from.address, blacklisted.address, 1), - ).revertedWith(acErrors.WMAC_HAS_ROLE); - }); - - it('burn(...) when address is blacklisted', async () => { - const { owner, regularAccounts, accessControl, tokenContract } = - await deployMTokenWithFixture(); - - const blacklisted = regularAccounts[0]; - - await mint({ tokenContract, owner }, blacklisted, 1); - await blackList( - { blacklistable: tokenContract, accessControl, owner }, - blacklisted, - ); - await burn({ tokenContract, owner }, blacklisted, 1); - }); - - it('transferFrom(...) when caller address is blacklisted', async () => { - const { owner, regularAccounts, accessControl, tokenContract } = - await deployMTokenWithFixture(); - - const blacklisted = regularAccounts[0]; - const from = regularAccounts[1]; - const to = regularAccounts[2]; - - await mint({ tokenContract, owner }, from, 1); - await blackList( - { blacklistable: tokenContract, accessControl, owner }, - blacklisted, - ); - - await tokenContract.connect(from).approve(blacklisted.address, 1); - - await expect( - tokenContract - .connect(blacklisted) - .transferFrom(from.address, to.address, 1), - ).not.reverted; - }); - - it('transfer(...) when caller address was blacklisted and then un-blacklisted', async () => { - const { owner, regularAccounts, accessControl, tokenContract } = - await deployMTokenWithFixture(); - - const blacklisted = regularAccounts[0]; - const to = regularAccounts[2]; - - await mint({ tokenContract, owner }, blacklisted, 1); - await blackList( - { blacklistable: tokenContract, accessControl, owner }, - blacklisted, - ); - - await expect( - tokenContract.connect(blacklisted).transfer(to.address, 1), - ).revertedWith(acErrors.WMAC_HAS_ROLE); - - await unBlackList( - { blacklistable: tokenContract, accessControl, owner }, - blacklisted, - ); - - await expect(tokenContract.connect(blacklisted).transfer(to.address, 1)) - .not.reverted; - }); - }); - }); - describe('roles check', () => { - it('DataFeed', async function () { - const fixture = await deployMTokenVaultsWithFixture(); - const dataFeed = fixture.tokenDataFeed as Contract; - - if (!dataFeed || !tokenRoleNames.customFeedAdmin || isTac) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this as any).skip(); - return; - } - - expect(await dataFeed.feedAdminRole()).eq( - await dataFeed[tokenRoleNames.customFeedAdmin](), - ); - expect(await dataFeed.feedAdminRole()).eq(tokenRoles.customFeedAdmin); - }); - - it('CustomAggregator', async function () { - const fixture = await deployMTokenVaultsWithFixture(); - const customAggregator = (fixture.tokenCustomAggregatorFeed ?? - fixture.tokenCustomAggregatorFeedGrowth) as Contract; - - if (!customAggregator || !tokenRoleNames.customFeedAdmin || isTac) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this as any).skip(); - return; - } - - expect(await customAggregator.feedAdminRole()).eq( - await customAggregator[tokenRoleNames.customFeedAdmin](), - ); - expect(await customAggregator.feedAdminRole()).eq( - tokenRoles.customFeedAdmin, - ); - }); - - it('DepositVault', async function () { - const fixture = await deployMTokenVaultsWithFixture(); - const depositVault = fixture.tokenDepositVault as Contract; - - if (!depositVault) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this as any).skip(); - return; - } - - expect(await depositVault.vaultRole()).eq( - token === 'mTBILL' - ? tokenRoles.depositVaultAdmin - : await depositVault[tokenRoleNames.depositVaultAdmin](), - ); - expect(await depositVault.vaultRole()).eq(tokenRoles.depositVaultAdmin); - }); - - it('DepositVaultWithUSTB', async function () { - const fixture = await deployMTokenVaultsWithFixture(); - const depositVaultUstb = fixture.tokenDepositVaultUstb as Contract; - - if (!depositVaultUstb) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this as any).skip(); - return; - } - - expect(await depositVaultUstb.vaultRole()).eq( - token === 'mTBILL' - ? tokenRoles.depositVaultAdmin - : await depositVaultUstb[tokenRoleNames.depositVaultAdmin](), - ); - expect(await depositVaultUstb.vaultRole()).eq( - tokenRoles.depositVaultAdmin, - ); - }); - - it('RedemptionVault', async function () { - const fixture = await deployMTokenVaultsWithFixture(); - const redemptionVault = fixture.tokenRedemptionVault as Contract; - - if (!redemptionVault) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this as any).skip(); - return; - } - - expect(await redemptionVault.vaultRole()).eq( - token === 'mTBILL' - ? tokenRoles.redemptionVaultAdmin - : await redemptionVault[tokenRoleNames.redemptionVaultAdmin](), - ); - expect(await redemptionVault.vaultRole()).eq( - tokenRoles.redemptionVaultAdmin, - ); - }); - - it('RedemptionVaultWithSwapper', async function () { - const fixture = await deployMTokenVaultsWithFixture(); - const redemptionVaultWithSwapper = - fixture.tokenRedemptionVaultWithSwapper as Contract; - - if (!redemptionVaultWithSwapper) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this as any).skip(); - return; - } - - expect(await redemptionVaultWithSwapper.vaultRole()).eq( - token === 'mTBILL' - ? tokenRoles.redemptionVaultAdmin - : await redemptionVaultWithSwapper[ - tokenRoleNames.redemptionVaultAdmin - ](), - ); - expect(await redemptionVaultWithSwapper.vaultRole()).eq( - tokenRoles.redemptionVaultAdmin, - ); - }); - - it('RedemptionVaultWithBUIDL', async function () { - const fixture = await deployMTokenVaultsWithFixture(); - const redemptionVaultWithBuidl = - fixture.tokenRedemptionVaultWithBuidl as Contract; - - if (!redemptionVaultWithBuidl) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this as any).skip(); - return; - } - - expect(await redemptionVaultWithBuidl.vaultRole()).eq( - token === 'mTBILL' - ? tokenRoles.redemptionVaultAdmin - : await redemptionVaultWithBuidl[ - tokenRoleNames.redemptionVaultAdmin - ](), - ); - expect(await redemptionVaultWithBuidl.vaultRole()).eq( - tokenRoles.redemptionVaultAdmin, - ); - }); - - it('vaults greenlistedRole()', async function () { - const fixture = await deployMTokenVaultsWithFixture(); - - const vaults = ( - [ - fixture.tokenDepositVault, - fixture.tokenDepositVaultUstb, - fixture.tokenRedemptionVault, - fixture.tokenRedemptionVaultWithSwapper, - fixture.tokenRedemptionVaultWithBuidl, - ] as (Contract | null)[] - ).filter((v): v is Contract => !!v); - - if (vaults.length === 0) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this as any).skip(); - return; - } - - // Token-level (separated) greenlist products override greenlistedRole() - // to return their configured role (mGLO reuses mGLOBAL's - // M_GLOBAL_GREENLISTED_ROLE via sharedGreenlistRoleSource); every other - // product inherits the shared common GREENLISTED_ROLE. - const expectedGreenlistedRole = tokenLevelGreenlistTokens.includes(token) - ? tokenRoles.greenlisted - : allRoles.common.greenlisted; - - for (const vault of vaults) { - expect(await vault.greenlistedRole()).eq(expectedGreenlistedRole); - } - }); - }); -}; diff --git a/test/common/vault-initializer.helpers.ts b/test/common/vault-initializer.helpers.ts new file mode 100644 index 00000000..bbf6f6b5 --- /dev/null +++ b/test/common/vault-initializer.helpers.ts @@ -0,0 +1,473 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { BigNumberish, constants, Contract, ContractTransaction } from 'ethers'; +import { ethers } from 'hardhat'; + +import { + AccountOrContract, + getAccount, + handleRevert, + OptionalCommonParams, +} from './common.helpers'; + +import { + DepositVaultTest, + DepositVaultTest__factory, + DepositVaultWithAaveTest, + DepositVaultWithAaveTest__factory, + DepositVaultWithMorphoTest, + DepositVaultWithMorphoTest__factory, + DepositVaultWithMTokenTest, + DepositVaultWithMTokenTest__factory, + DepositVaultWithUSTBTest, + DepositVaultWithUSTBTest__factory, + ManageableVaultTester, + ManageableVaultTester__factory, + RedemptionVaultTest, + RedemptionVaultTest__factory, + RedemptionVaultWithAaveTest, + RedemptionVaultWithAaveTest__factory, + RedemptionVaultWithMorphoTest, + RedemptionVaultWithMorphoTest__factory, + RedemptionVaultWithMTokenTest, + RedemptionVaultWithMTokenTest__factory, + RedemptionVaultWithUSTBTest, + RedemptionVaultWithUSTBTest__factory, +} from '../../typechain-types'; + +export const DV_USTB_INIT_FN = + 'initialize((uint256,uint256,uint256,address,address,address,address,address,uint256,uint256,uint256,uint256,bool),(uint256,uint256,uint256),address)'; + +export const DV_MTOKEN_INIT_FN = DV_USTB_INIT_FN; + +export const RV_USTB_INIT_FN = + 'initialize((uint256,uint256,uint256,address,address,address,address,address,uint256,uint256,uint256,uint256,bool),(address,address,address,address,uint256),address)'; + +export const RV_MTOKEN_INIT_FN = RV_USTB_INIT_FN; + +export type InitializerParamsMv = { + accessControl: AccountOrContract; + mockedSanctionsList: AccountOrContract; + mTBILL: AccountOrContract; + mTokenToUsdDataFeed: AccountOrContract; + tokensReceiver: AccountOrContract; + minAmount?: BigNumberish; + instantFee?: BigNumberish; + minInstantFee?: BigNumberish; + maxInstantFee?: BigNumberish; + maxInstantShare?: BigNumberish; + variationTolerance?: BigNumberish; + sequentialRequestProcessing?: boolean; + maxApproveRequestId?: BigNumberish; +}; + +export type InitializerParamsDv = { + maxAmountPerRequest?: BigNumberish; + minMTokenAmountForFirstDeposit?: BigNumberish; + maxSupplyCap?: BigNumberish; +} & InitializerParamsMv; + +export type InitializerParamsDvWithUstb = { + ustbToken: AccountOrContract; +} & InitializerParamsDv; + +export type InitializerParamsDvWithMToken = { + depositVault: AccountOrContract; +} & InitializerParamsDv; + +export type InitializerParamsRv = { + requestRedeemer: AccountOrContract; + loanLp?: AccountOrContract; + loanRepaymentAddress?: AccountOrContract; + redemptionVaultLoanSwapper?: AccountOrContract; + loanApr?: BigNumberish; +} & InitializerParamsMv; + +export type InitializerParamsRvWithMToken = { + redemptionVault: AccountOrContract; +} & InitializerParamsRv; + +export type InitializerParamsRvWithUstb = { + ustbRedemption: AccountOrContract; +} & InitializerParamsRv; + +const resolveDeployer = async (opt?: OptionalCommonParams) => { + if (opt?.from) { + return opt.from; + } + + const [deployer] = await ethers.getSigners(); + return deployer; +}; + +const initializeContract = async ( + contract: TContract, + initFn: () => Promise, + opt?: OptionalCommonParams, +): Promise => { + if (await handleRevert(initFn, contract as Contract, opt)) { + return contract; + } + + await expect(initFn()).to.not.be.reverted; + return contract; +}; + +const deployIfNeeded = async ( + contract: TContract | undefined, + deploy: (deployer: SignerWithAddress) => Promise, + opt?: OptionalCommonParams, +): Promise<{ contract: TContract; deployer: SignerWithAddress }> => { + const deployer = await resolveDeployer(opt); + + if (contract) { + return { contract, deployer }; + } + + return { + contract: await deploy(deployer), + deployer, + }; +}; + +export const getInitializerParamsMv = ({ + accessControl, + mockedSanctionsList, + mTBILL, + mTokenToUsdDataFeed, + tokensReceiver, + minAmount, + instantFee, + minInstantFee, + maxInstantFee, + maxInstantShare, + variationTolerance, + sequentialRequestProcessing, + maxApproveRequestId, +}: InitializerParamsMv) => { + return [ + { + ac: getAccount(accessControl), + sanctionsList: getAccount(mockedSanctionsList), + variationTolerance: variationTolerance ?? 1, + minAmount: minAmount ?? 1000, + mToken: getAccount(mTBILL), + mTokenDataFeed: getAccount(mTokenToUsdDataFeed), + tokensReceiver: getAccount(tokensReceiver), + instantFee: instantFee ?? 100, + minInstantFee: minInstantFee ?? 0, + maxInstantFee: maxInstantFee ?? 10000, + maxInstantShare: maxInstantShare ?? 100_00, + sequentialRequestProcessing: sequentialRequestProcessing ?? false, + maxApproveRequestId: maxApproveRequestId ?? 100, + }, + ] as const; +}; + +export const getInitializerParamsRv = ( + { + requestRedeemer, + loanLp, + loanRepaymentAddress, + redemptionVaultLoanSwapper, + loanApr, + ...commonParams + }: InitializerParamsRv, + extraParams?: TExtraParams, +) => { + return [ + ...getInitializerParamsMv(commonParams), + { + requestRedeemer: getAccount(requestRedeemer), + loanLp: getAccount(loanLp ?? constants.AddressZero), + loanRepaymentAddress: getAccount( + loanRepaymentAddress ?? constants.AddressZero, + ), + loanSwapperVault: getAccount( + redemptionVaultLoanSwapper ?? constants.AddressZero, + ), + loanApr: loanApr ?? 0, + }, + ...(extraParams ?? []), + ] as const; +}; + +export const getInitializerParamsRvWithMToken = ({ + redemptionVault, + ...commonParams +}: InitializerParamsRvWithMToken) => { + return [ + ...getInitializerParamsRv(commonParams), + getAccount(redemptionVault), + ] as const; +}; + +export const getInitializerParamsRvWithUstb = ({ + ustbRedemption, + ...commonParams +}: InitializerParamsRvWithUstb) => { + return [ + ...getInitializerParamsRv(commonParams), + getAccount(ustbRedemption), + ] as const; +}; + +export const getInitializerParamsDv = ({ + maxAmountPerRequest, + minMTokenAmountForFirstDeposit, + maxSupplyCap, + ...commonParams +}: InitializerParamsDv) => { + return [ + ...getInitializerParamsMv(commonParams), + { + minMTokenAmountForFirstDeposit: minMTokenAmountForFirstDeposit ?? 0, + maxSupplyCap: maxSupplyCap ?? constants.MaxUint256, + maxAmountPerRequest: maxAmountPerRequest ?? constants.MaxUint256, + }, + ] as const; +}; + +export const getInitializerParamsDvWithUstb = ({ + ustbToken, + ...commonParams +}: InitializerParamsDvWithUstb) => { + return [ + ...getInitializerParamsDv(commonParams), + getAccount(ustbToken), + ] as const; +}; + +export const getInitializerParamsDvWithMToken = ({ + depositVault, + ...commonParams +}: InitializerParamsDvWithMToken) => { + return [ + ...getInitializerParamsDv(commonParams), + getAccount(depositVault), + ] as const; +}; + +export const initializeMv = async ( + params: InitializerParamsMv, + contract?: ManageableVaultTester, + opt?: OptionalCommonParams, +): Promise => { + const { contract: vault, deployer } = await deployIfNeeded( + contract, + (deployer) => new ManageableVaultTester__factory(deployer).deploy(), + opt, + ); + const initParams = getInitializerParamsMv(params); + + return initializeContract( + vault, + () => + vault.connect(opt?.from ?? deployer).initializeExternal(...initParams), + opt, + ); +}; + +const initializeStandardDv = async < + TContract extends + | DepositVaultTest + | DepositVaultWithAaveTest + | DepositVaultWithMorphoTest, +>( + params: InitializerParamsDv, + contract: TContract | undefined, + deploy: (deployer: SignerWithAddress) => Promise, + opt?: OptionalCommonParams, +): Promise => { + const { contract: vault, deployer } = await deployIfNeeded( + contract, + deploy, + opt, + ); + const from = opt?.from ?? deployer; + const initParams = getInitializerParamsDv(params); + + return initializeContract( + vault, + () => vault.connect(from).initialize(...initParams), + opt, + ); +}; + +export const initializeDv = ( + params: InitializerParamsDv, + contract?: DepositVaultTest, + opt?: OptionalCommonParams, +) => + initializeStandardDv( + params, + contract, + (deployer) => new DepositVaultTest__factory(deployer).deploy(), + opt, + ); + +export const initializeDvWithAave = ( + params: InitializerParamsDv, + contract?: DepositVaultWithAaveTest, + opt?: OptionalCommonParams, +) => + initializeStandardDv( + params, + contract, + (deployer) => new DepositVaultWithAaveTest__factory(deployer).deploy(), + opt, + ); + +export const initializeDvWithMorpho = ( + params: InitializerParamsDv, + contract?: DepositVaultWithMorphoTest, + opt?: OptionalCommonParams, +) => + initializeStandardDv( + params, + contract, + (deployer) => new DepositVaultWithMorphoTest__factory(deployer).deploy(), + opt, + ); + +export const initializeDvWithUstb = async ( + params: InitializerParamsDvWithUstb, + contract?: DepositVaultWithUSTBTest, + opt?: OptionalCommonParams, +): Promise => { + const { contract: vault, deployer } = await deployIfNeeded( + contract, + (deployer) => new DepositVaultWithUSTBTest__factory(deployer).deploy(), + opt, + ); + const from = opt?.from ?? deployer; + const initParams = getInitializerParamsDvWithUstb(params); + + return initializeContract( + vault, + () => vault.connect(from)[DV_USTB_INIT_FN](...initParams), + opt, + ); +}; + +export const initializeDvWithMToken = async ( + params: InitializerParamsDvWithMToken, + contract?: DepositVaultWithMTokenTest, + opt?: OptionalCommonParams, +): Promise => { + const { contract: vault, deployer } = await deployIfNeeded( + contract, + (deployer) => new DepositVaultWithMTokenTest__factory(deployer).deploy(), + opt, + ); + const from = opt?.from ?? deployer; + const initParams = getInitializerParamsDvWithMToken(params); + + return initializeContract( + vault, + () => vault.connect(from)[DV_MTOKEN_INIT_FN](...initParams), + opt, + ); +}; + +const initializeStandardRv = async < + TContract extends + | RedemptionVaultTest + | RedemptionVaultWithAaveTest + | RedemptionVaultWithMorphoTest, +>( + params: InitializerParamsRv, + contract: TContract | undefined, + deploy: (deployer: SignerWithAddress) => Promise, + opt?: OptionalCommonParams, +): Promise => { + const { contract: vault, deployer } = await deployIfNeeded( + contract, + deploy, + opt, + ); + const from = opt?.from ?? deployer; + const initParams = getInitializerParamsRv(params); + + return initializeContract( + vault, + () => vault.connect(from).initialize(...initParams), + opt, + ); +}; + +export const initializeRv = ( + params: InitializerParamsRv, + contract?: RedemptionVaultTest, + opt?: OptionalCommonParams, +) => + initializeStandardRv( + params, + contract, + (deployer) => new RedemptionVaultTest__factory(deployer).deploy(), + opt, + ); + +export const initializeRvWithAave = ( + params: InitializerParamsRv, + contract?: RedemptionVaultWithAaveTest, + opt?: OptionalCommonParams, +) => + initializeStandardRv( + params, + contract, + (deployer) => new RedemptionVaultWithAaveTest__factory(deployer).deploy(), + opt, + ); + +export const initializeRvWithMorpho = ( + params: InitializerParamsRv, + contract?: RedemptionVaultWithMorphoTest, + opt?: OptionalCommonParams, +) => + initializeStandardRv( + params, + contract, + (deployer) => new RedemptionVaultWithMorphoTest__factory(deployer).deploy(), + opt, + ); + +export const initializeRvWithUstb = async ( + params: InitializerParamsRvWithUstb, + contract?: RedemptionVaultWithUSTBTest, + opt?: OptionalCommonParams, +): Promise => { + const { contract: vault, deployer } = await deployIfNeeded( + contract, + (deployer) => new RedemptionVaultWithUSTBTest__factory(deployer).deploy(), + opt, + ); + const from = opt?.from ?? deployer; + const initParams = getInitializerParamsRvWithUstb(params); + + return initializeContract( + vault, + () => vault.connect(from)[RV_USTB_INIT_FN](...initParams), + opt, + ); +}; + +export const initializeRvWithMToken = async ( + params: InitializerParamsRvWithMToken, + contract?: RedemptionVaultWithMTokenTest, + opt?: OptionalCommonParams, +): Promise => { + const { contract: vault, deployer } = await deployIfNeeded( + contract, + (deployer) => new RedemptionVaultWithMTokenTest__factory(deployer).deploy(), + opt, + ); + const from = opt?.from ?? deployer; + const initParams = getInitializerParamsRvWithMToken(params); + + return initializeContract( + vault, + () => vault.connect(from)[RV_MTOKEN_INIT_FN](...initParams), + opt, + ); +}; diff --git a/test/common/with-sanctions-list.helpers.ts b/test/common/with-sanctions-list.helpers.ts index 55271586..4842ff00 100644 --- a/test/common/with-sanctions-list.helpers.ts +++ b/test/common/with-sanctions-list.helpers.ts @@ -1,7 +1,12 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { Account, OptionalCommonParams, getAccount } from './common.helpers'; +import { + Account, + OptionalCommonParams, + getAccount, + handleRevert, +} from './common.helpers'; import { SanctionsListMock, WithSanctionsList } from '../../typechain-types'; @@ -28,26 +33,27 @@ export const setSanctionsList = async ( newSanctionsList: Account, opt?: OptionalCommonParams, ) => { + const sender = opt?.from ?? owner; newSanctionsList = getAccount(newSanctionsList); - if (opt?.revertMessage) { - await expect( + if ( + await handleRevert( withSanctionsList - .connect(opt?.from ?? owner) - .setSanctionsList(newSanctionsList), - ).revertedWith(opt?.revertMessage); + .connect(sender) + .setSanctionsList.bind(this, newSanctionsList), + withSanctionsList, + opt, + ) + ) { return; } await expect( - withSanctionsList - .connect(opt?.from ?? owner) - .setSanctionsList(newSanctionsList), + withSanctionsList.connect(sender).setSanctionsList(newSanctionsList), ).to.emit( withSanctionsList, - withSanctionsList.interface.events['SetSanctionsList(address,address)'] - .name, - ).to.not.reverted; + withSanctionsList.interface.events['SetSanctionsList(address)'].name, + ); expect(await withSanctionsList.sanctionsList()).eq(newSanctionsList); }; diff --git a/test/integration/ChainlinkAdapters.test.ts b/test/integration/ChainlinkAdapters.test.ts index d685f59d..ff9f6b88 100644 --- a/test/integration/ChainlinkAdapters.test.ts +++ b/test/integration/ChainlinkAdapters.test.ts @@ -17,7 +17,10 @@ for (const networkKey in configsPerNetwork) { const config = configsPerNetwork[network as Network]!; describe(`Chainlink Adapters on ${network}`, function () { - this.timeout(120000); + // Each adapter's first fixture load resets the mainnet fork, which can take + // ~2min over RPC on its own; give the suite headroom so slow forks (e.g. the + // Syrup adapters) don't flake on the timeout. + this.timeout(300000); if (config.syrupAdapters?.length) { syrupAdaptersSuits( diff --git a/test/integration/ContractsUpgrade.test.ts b/test/integration/ContractsUpgrade.test.ts index bd403410..8b24a4d6 100644 --- a/test/integration/ContractsUpgrade.test.ts +++ b/test/integration/ContractsUpgrade.test.ts @@ -1,412 +1,1907 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { increase } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time'; +import { days } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time/duration'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { BytesLike, constants } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; -import { hyperEvmUpgradeFixture } from './fixtures/upgrades.fixture'; +import { mainnetUpgradeFixture } from './fixtures/upgrades.fixture'; -import { approveBase18 } from '../common/common.helpers'; +import { getAllRoles, getRolesForToken } from '../../helpers/roles'; import { - depositInstantTest, - depositRequestTest, - safeApproveRequestTest, - safeBulkApproveRequestTest as safeBulkApproveDepositRequestTest, -} from '../common/deposit-vault.helpers'; -import { mint } from '../common/mTBILL.helpers'; + AggregatorV3Mock__factory, + MidasAccessControl, + MidasAccessControlTimelockController, + MidasTimelockManager, +} from '../../typechain-types'; +import { acErrors, grantRoleMultTester } from '../common/ac.helpers'; +import { pauseGlobalTest } from '../common/common.helpers'; +import { burn, mint } from '../common/mtoken.helpers'; import { - redeemInstantTest, - redeemRequestTest, - safeApproveRedeemRequestTest, - safeBulkApproveRequestTest as safeBulkApproveRedeemRequestTest, -} from '../common/redemption-vault.helpers'; + executeTimelockOperationTester, + bulkScheduleTimelockOperationTester, +} from '../common/timelock-manager.helpers'; -describe.skip('ContractsUpgrade - HyperEVM Fork Integration Tests', function () { +describe('ContractsUpgrade - Mainnet Upgrade Integration Tests', function () { this.timeout(120000); - it('deposit instant', async function () { - const { - xaut0Whale, - xaut0, - vaultsManager, - depositVault, - xbxaut, - xbxautDataFeed, - } = await loadFixture(hyperEvmUpgradeFixture); - - await approveBase18(xaut0Whale, xaut0, depositVault, 1); - - await depositInstantTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - depositVault, - owner: vaultsManager, - }, - xaut0, - 1, - { from: xaut0Whale }, - ); - }); + const resetTimelockDelays = async ({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }: { + timelockManager: MidasTimelockManager; + acDefaultAdmin: SignerWithAddress; + accessControl: MidasAccessControl; + timelock: MidasAccessControlTimelockController; + }) => { + const roles = getAllRoles(); + + const rolesToReset = [ + roles.common.defaultAdmin, + roles.common.pauseAdmin, + roles.common.timelockOperationPauser, + roles.common.securityCouncilManager, + roles.common.greenlistedOperator, + roles.common.blacklistedOperator, + roles.tokenRoles.mTBILL.minter, + roles.tokenRoles.mTBILL.burner, + roles.tokenRoles.mTBILL.pauser, + roles.tokenRoles.mGLOBAL.minter, + roles.tokenRoles.mGLOBAL.burner, + roles.tokenRoles.mGLOBAL.pauser, + roles.tokenRoles.mTBILL.customFeedAdmin as string, + roles.tokenRoles.mGLOBAL.customFeedAdmin as string, + roles.common.greenlisted, + roles.common.blacklisted, + ]; + + // await setRoleTimelocksAndExecute( + // { + // timelockManager, + // accessControl, + // timelock, + // owner: acDefaultAdmin, + // }, + // rolesToReset.map((role) => ({ + // role, + // delay: NO_DELAY, + // })), + // ); + }; + + // every role has a default timelock delay of 1 hour when no explicit delay is set + const DEFAULT_TIMELOCK_DELAY = 3600; + + type TimelockContext = { + timelockManager: MidasTimelockManager; + timelock: MidasAccessControlTimelockController; + accessControl: MidasAccessControl; + owner: SignerWithAddress; + }; + + /** + * Schedules an operation through the timelock manager, waits the default + * delay and executes it. Mirrors the flow used in MidasTimelockManager.test.ts. + */ + const executeWriteViaTimelock = async ( + ctx: TimelockContext, + target: string, + calldata: BytesLike, + proposer: SignerWithAddress, + { + delay = DEFAULT_TIMELOCK_DELAY, + checkAndSetDefaultDelay = true, + }: { delay?: number; checkAndSetDefaultDelay?: boolean } = {}, + ) => { + if (checkAndSetDefaultDelay) { + const defaultDelay = await ctx.accessControl.defaultDelay(); + + if (defaultDelay === 0) { + await executeWriteViaTimelock( + ctx, + ctx.accessControl.address, + ctx.accessControl.interface.encodeFunctionData('setDefaultDelay', [ + delay, + ]), + ctx.owner, + { delay: days(2), checkAndSetDefaultDelay: false }, + ); + } + } - it('deposit instant with custom recipient', async function () { - const { - xaut0Whale, - xaut0, - vaultsManager, - depositVault, - xbxaut, - xbxautDataFeed, - customRecipient, - } = await loadFixture(hyperEvmUpgradeFixture); - - await approveBase18(xaut0Whale, xaut0, depositVault, 1); - - await depositInstantTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - depositVault, - owner: vaultsManager, - customRecipient, - }, - xaut0, - 1, - { from: xaut0Whale }, + await bulkScheduleTimelockOperationTester( + ctx, + [target], + [calldata as string], + {}, + { from: proposer }, ); - }); - it('deposit request', async function () { - const { - xaut0Whale, - xaut0, - vaultsManager, - depositVault, - xbxaut, - xbxautDataFeed, - } = await loadFixture(hyperEvmUpgradeFixture); - - await approveBase18(xaut0Whale, xaut0, depositVault, 1); - - await depositRequestTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - depositVault, - owner: vaultsManager, - }, - xaut0, - 1, - { from: xaut0Whale }, + await increase(delay + 1); + + await executeTimelockOperationTester( + ctx, + target, + calldata as string, + proposer.address, + { from: ctx.owner }, ); - }); + }; - it('deposit request approve', async function () { - const { - xaut0Whale, - xaut0, - vaultsManager, - depositVault, - xbxaut, - xbxautDataFeed, - } = await loadFixture(hyperEvmUpgradeFixture); - - await approveBase18(xaut0Whale, xaut0, depositVault, 1); - - const { requestId, rate } = await depositRequestTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - depositVault, - owner: vaultsManager, - }, - xaut0, - 1, - { from: xaut0Whale }, + /** + * Grants a role to `account` through the timelock flow (no delay reset). + * The proposer must hold the admin role of `role` (DEFAULT_ADMIN_ROLE). + */ + const grantRoleViaTimelock = async ( + ctx: TimelockContext, + role: string, + account: string, + proposer: SignerWithAddress, + ) => { + await executeWriteViaTimelock( + ctx, + ctx.accessControl.address, + ctx.accessControl.interface.encodeFunctionData( + 'grantRole(bytes32,address)', + [role, account], + ), + proposer, ); + }; - await safeApproveRequestTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - depositVault, - owner: vaultsManager, - }, - requestId!, - rate!, + /** + * Grants multiple roles (sharing the same admin role) through the timelock. + */ + const grantRoleMultViaTimelock = async ( + ctx: TimelockContext, + roles: string[], + accounts: string[], + proposer: SignerWithAddress, + ) => { + const params = roles.map((role, index) => ({ + role, + account: accounts[index], + delay: 0, + })); + await executeWriteViaTimelock( + ctx, + ctx.accessControl.address, + ctx.accessControl.interface.encodeFunctionData('grantRoleMult', [params]), + proposer, ); + }; + describe('MidasAccessControl', () => { + describe('initializeRelationships()', () => { + it('should expose deployed pause and timelock managers', async () => { + const { accessControl, pauseManager, timelockManager } = + await loadFixture(mainnetUpgradeFixture); + + expect(await accessControl.pauseManager()).to.eq(pauseManager.address); + expect(await accessControl.timelockManager()).to.eq( + timelockManager.address, + ); + }); + }); + + describe('grantRole()', () => { + it('should grant role to account', async () => { + const { accessControl, acDefaultAdmin, timelockManager, timelock } = + await loadFixture(mainnetUpgradeFixture); + const roles = getAllRoles(); + const [, recipient] = await ethers.getSigners(); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await expect( + accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + roles.common.pauseAdmin, + recipient.address, + ), + ).not.reverted; + + expect( + await accessControl.hasRole( + roles.common.pauseAdmin, + recipient.address, + ), + ).to.eq(true); + }); + + it('should fail: when caller has no admin role', async () => { + const { accessControl, acDefaultAdmin, timelockManager, timelock } = + await loadFixture(mainnetUpgradeFixture); + const roles = getAllRoles(); + const [, recipient, unauthorized] = await ethers.getSigners(); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await expect( + accessControl + .connect(unauthorized) + ['grantRole(bytes32,address)']( + roles.common.pauseAdmin, + recipient.address, + ), + ).revertedWithCustomError( + accessControl, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + + it('should grant role via timelock', async () => { + const { accessControl, acDefaultAdmin, timelockManager, timelock } = + await loadFixture(mainnetUpgradeFixture); + const roles = getAllRoles(); + const [, recipient] = await ethers.getSigners(); + + await grantRoleViaTimelock( + { timelockManager, timelock, accessControl, owner: acDefaultAdmin }, + roles.common.pauseAdmin, + recipient.address, + acDefaultAdmin, + ); + + expect( + await accessControl.hasRole( + roles.common.pauseAdmin, + recipient.address, + ), + ).to.eq(true); + }); + }); + + describe('grantRoleMult()', () => { + it('should grant multiple roles', async () => { + const { accessControl, acDefaultAdmin, timelockManager, timelock } = + await loadFixture(mainnetUpgradeFixture); + const roles = getAllRoles(); + const [, accountA, accountB] = await ethers.getSigners(); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await grantRoleMultTester( + { accessControl, owner: acDefaultAdmin }, + [ + { + role: roles.common.pauseAdmin, + account: accountA.address, + delay: 0, + }, + { + role: roles.common.timelockOperationPauser, + account: accountB.address, + delay: 0, + }, + ], + { + from: acDefaultAdmin, + }, + ); + }); + + it('should fail: array is empty', async () => { + const { accessControl, acDefaultAdmin, timelockManager, timelock } = + await loadFixture(mainnetUpgradeFixture); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await grantRoleMultTester( + { accessControl, owner: acDefaultAdmin }, + [], + { + from: acDefaultAdmin, + revertCustomError: { customErrorName: 'EmptyArray' }, + }, + ); + }); + }); }); - it('deposit request approve bulk', async function () { - const { - xaut0Whale, - xaut0, - vaultsManager, - depositVault, - xbxaut, - xbxautDataFeed, - } = await loadFixture(hyperEvmUpgradeFixture); - - await approveBase18(xaut0Whale, xaut0, depositVault, 1); - - const { requestId } = await depositRequestTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - depositVault, - owner: vaultsManager, - }, - xaut0, - 1, - { from: xaut0Whale }, - ); + describe('MidasPauseManager', () => { + describe('globalPause()', () => { + it('should pause globally when called by pause admin', async () => { + const { + accessControl, + pauseManager, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const roles = getAllRoles(); - await safeBulkApproveDepositRequestTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - depositVault, - owner: vaultsManager, - }, - [{ id: requestId! }], - ); + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + roles.common.pauseAdmin, + acDefaultAdmin.address, + ); + + await pauseGlobalTest({ + pauseManager, + owner: acDefaultAdmin, + }); + }); + + it('should fail: when caller has no pause admin permission', async () => { + const { + pauseManager, + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [, unauthorized] = await ethers.getSigners(); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await pauseGlobalTest( + { pauseManager, owner: unauthorized }, + { + from: unauthorized, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('should fail: pause globally via timelock when pause delay is 0', async () => { + const { + accessControl, + pauseManager, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const roles = getAllRoles(); + + const ctx = { + timelockManager, + timelock, + accessControl, + owner: acDefaultAdmin, + }; + + await grantRoleViaTimelock( + ctx, + roles.common.pauseAdmin, + acDefaultAdmin.address, + acDefaultAdmin, + ); + + await bulkScheduleTimelockOperationTester( + ctx, + [pauseManager.address], + [pauseManager.interface.encodeFunctionData('globalPause') as string], + {}, + { + from: acDefaultAdmin, + revertCustomError: { + contract: timelockManager, + customErrorName: 'NoTimelockDelayForRole', + }, + }, + ); + }); + }); }); - it('deposit instant with custom recipient', async function () { - const { - xaut0Whale, - xaut0, - vaultsManager, - depositVault, - xbxaut, - xbxautDataFeed, - customRecipient, - } = await loadFixture(hyperEvmUpgradeFixture); - - await approveBase18(xaut0Whale, xaut0, depositVault, 1); - - await depositRequestTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - depositVault, - owner: vaultsManager, - customRecipient, - }, - xaut0, - 1, - { from: xaut0Whale }, - ); + describe('MidasTimelockManager', () => { + describe('deployment', () => { + it('should link to access control and timelock controller', async () => { + const { + accessControl, + timelockManager, + timelock, + securityCouncilMembers, + } = await loadFixture(mainnetUpgradeFixture); + + expect(await timelockManager.accessControl()).to.eq( + accessControl.address, + ); + expect(await timelockManager.timelock()).to.eq(timelock.address); + expect(await timelockManager.councilQuorum(0)).to.eq(3); + expect(await timelockManager.getSecurityCouncilMembers(0)).to.deep.eq( + securityCouncilMembers.map((s) => s.address), + ); + }); + }); }); - it('redeem instant', async function () { - const { - xaut0Whale, - xaut0, - vaultsManager, - redemptionVaultSwapper, - xbxaut, - xbxautDataFeed, - tokenManager, - } = await loadFixture(hyperEvmUpgradeFixture); - - await mint( - { tokenContract: xbxaut, owner: tokenManager }, - xaut0Whale, - parseUnits('1'), - ); + describe('mTBILL', () => { + const mTbillRoles = getRolesForToken('mTBILL'); - await approveBase18(xaut0Whale, xbxaut, redemptionVaultSwapper, 1); - - await redeemInstantTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - redemptionVault: redemptionVaultSwapper, - owner: vaultsManager, - }, - xaut0, - 1, - { from: xaut0Whale }, - ); + describe('mint()', () => { + it('should mint tokens to recipient', async () => { + const { + mTbill, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [, recipient] = await ethers.getSigners(); + const amount = parseUnits('1'); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + mTbillRoles.minter, + acDefaultAdmin.address, + ); + + await mint( + { tokenContract: mTbill, owner: acDefaultAdmin }, + recipient.address, + amount, + ); + + expect(await mTbill.balanceOf(recipient.address)).to.eq(amount); + }); + + it('should fail: when caller has no mint operator role', async () => { + const { + mTbill, + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [, recipient, unauthorized] = await ethers.getSigners(); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await expect( + mTbill.connect(unauthorized).mint(recipient.address, 1), + ).revertedWithCustomError( + mTbill, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + + it('should fail: when trying to schedule mint through timelock', async () => { + const { + mTbill, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [, recipient] = await ethers.getSigners(); + const amount = parseUnits('1'); + + const ctx = { + timelockManager, + timelock, + accessControl, + owner: acDefaultAdmin, + }; + + await grantRoleViaTimelock( + ctx, + mTbillRoles.minter, + acDefaultAdmin.address, + acDefaultAdmin, + ); + + await bulkScheduleTimelockOperationTester( + ctx, + [mTbill.address], + [ + mTbill.interface.encodeFunctionData('mint', [ + recipient.address, + amount, + ]), + ], + {}, + { + from: acDefaultAdmin, + revertCustomError: { + contract: timelockManager, + customErrorName: 'InvalidPreflightError', + }, + }, + ); + }); + }); + + describe('burn()', () => { + it('should burn tokens from holder', async () => { + const { + mTbill, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [, holder] = await ethers.getSigners(); + const amount = parseUnits('1'); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + mTbillRoles.minter, + acDefaultAdmin.address, + ); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + mTbillRoles.burner, + acDefaultAdmin.address, + ); + + await mint( + { tokenContract: mTbill, owner: acDefaultAdmin }, + holder.address, + amount, + ); + + await burn( + { tokenContract: mTbill, owner: acDefaultAdmin }, + holder.address, + amount, + ); + + expect(await mTbill.balanceOf(holder.address)).to.eq(0); + }); + + it('should fail: when caller has no burn operator role', async () => { + const { + mTbill, + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [, holder, unauthorized] = await ethers.getSigners(); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await expect( + mTbill.connect(unauthorized).burn(holder.address, 1), + ).revertedWithCustomError( + mTbill, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + + it('should fail: when trying to schedule burn through timelock', async () => { + const { + mTbill, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [, holder] = await ethers.getSigners(); + const amount = parseUnits('1'); + + const ctx = { + timelockManager, + timelock, + accessControl, + owner: acDefaultAdmin, + }; + + await grantRoleMultViaTimelock( + ctx, + [mTbillRoles.minter, mTbillRoles.burner], + [acDefaultAdmin.address, acDefaultAdmin.address], + acDefaultAdmin, + ); + + await mint( + { tokenContract: mTbill, owner: acDefaultAdmin }, + holder.address, + amount, + ); + + await bulkScheduleTimelockOperationTester( + ctx, + [mTbill.address], + [ + mTbill.interface.encodeFunctionData('burn', [ + holder.address, + amount, + ]), + ], + {}, + { + from: acDefaultAdmin, + revertCustomError: { + contract: timelockManager, + customErrorName: 'InvalidPreflightError', + }, + }, + ); + }); + }); + + describe('transfer()', () => { + it('should transfer between token holders', async function () { + const { mTbill, mTbillHolders } = await loadFixture( + mainnetUpgradeFixture, + ); + + const from = mTbillHolders[0]; + const to = mTbillHolders[1]; + const amount = parseUnits('0.01'); + + await expect(mTbill.connect(from).transfer(to.address, amount)).not + .reverted; + + expect(await mTbill.balanceOf(to.address)).to.be.gte(amount); + }); + + it('should fail: when sender is blacklisted', async () => { + const { + mTbill, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [, from, to] = await ethers.getSigners(); + const amount = parseUnits('1'); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + const allRoles = await getAllRoles(); + await grantRoleMultTester( + { accessControl, owner: acDefaultAdmin }, + [ + { + role: mTbillRoles.minter, + account: acDefaultAdmin.address, + delay: 0, + }, + { + role: allRoles.common.blacklistedOperator, + account: acDefaultAdmin.address, + delay: 0, + }, + ], + { from: acDefaultAdmin }, + ); + + await mint( + { tokenContract: mTbill, owner: acDefaultAdmin }, + from.address, + amount, + ); + + const roles = getAllRoles(); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + roles.common.blacklisted, + from.address, + ); + + await expect( + mTbill.connect(from).transfer(to.address, amount), + ).revertedWithCustomError( + mTbill, + acErrors.WMAC_BLACKLISTED().customErrorName, + ); + }); + }); }); - it('redeem instant with custom recipient', async function () { - const { - xaut0Whale, - xaut0, - vaultsManager, - redemptionVaultSwapper, - xbxaut, - xbxautDataFeed, - tokenManager, - customRecipient, - } = await loadFixture(hyperEvmUpgradeFixture); - - await mint( - { tokenContract: xbxaut, owner: tokenManager }, - xaut0Whale, - parseUnits('1'), - ); + describe('mGLOBAL', () => { + const mGlobalRoles = getRolesForToken('mGLOBAL'); - await approveBase18(xaut0Whale, xbxaut, redemptionVaultSwapper, 1); - - await redeemInstantTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - redemptionVault: redemptionVaultSwapper, - owner: vaultsManager, - customRecipient, - }, - xaut0, - 1, - { from: xaut0Whale }, - ); + describe('mint()', () => { + it('should mint tokens to greenlisted recipient', async () => { + const { + mGlobal, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [, recipient] = await ethers.getSigners(); + const amount = parseUnits('1'); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + mGlobalRoles.minter, + acDefaultAdmin.address, + ); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + mGlobalRoles.greenlisted, + recipient.address, + ); + + await mint( + { tokenContract: mGlobal, owner: acDefaultAdmin }, + recipient.address, + amount, + ); + + expect(await mGlobal.balanceOf(recipient.address)).to.eq(amount); + }); + + it('should fail: when caller has no mint operator role', async () => { + const { + mGlobal, + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [, recipient, unauthorized] = await ethers.getSigners(); + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await expect( + mGlobal.connect(unauthorized).mint(recipient.address, 1), + ).revertedWithCustomError( + mGlobal, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + + it('should fail: when trying to schedule mint through timelock', async () => { + const { + mGlobal, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [, recipient] = await ethers.getSigners(); + const amount = parseUnits('1'); + + const ctx = { + timelockManager, + timelock, + accessControl, + owner: acDefaultAdmin, + }; + + await grantRoleViaTimelock( + ctx, + mGlobalRoles.minter, + acDefaultAdmin.address, + acDefaultAdmin, + ); + await grantRoleViaTimelock( + ctx, + mGlobalRoles.greenlisted, + recipient.address, + acDefaultAdmin, + ); + + await bulkScheduleTimelockOperationTester( + ctx, + [mGlobal.address], + [ + mGlobal.interface.encodeFunctionData('mint', [ + recipient.address, + amount, + ]), + ], + {}, + { + from: acDefaultAdmin, + revertCustomError: { + contract: timelockManager, + customErrorName: 'InvalidPreflightError', + }, + }, + ); + }); + }); + + describe('burn()', () => { + it('should burn tokens from greenlisted holder', async () => { + const { + mGlobal, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [, holder] = await ethers.getSigners(); + const amount = parseUnits('1'); + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + mGlobalRoles.minter, + acDefaultAdmin.address, + ); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + mGlobalRoles.burner, + acDefaultAdmin.address, + ); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + mGlobalRoles.greenlisted, + holder.address, + ); + + await mint( + { tokenContract: mGlobal, owner: acDefaultAdmin }, + holder.address, + amount, + ); + + await burn( + { tokenContract: mGlobal, owner: acDefaultAdmin }, + holder.address, + amount, + ); + + expect(await mGlobal.balanceOf(holder.address)).to.eq(0); + }); + + it('should fail: when trying to schedule burn through timelock', async () => { + const { + mGlobal, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [, holder] = await ethers.getSigners(); + const amount = parseUnits('1'); + + const ctx = { + timelockManager, + timelock, + accessControl, + owner: acDefaultAdmin, + }; + + await grantRoleMultViaTimelock( + ctx, + [mGlobalRoles.minter, mGlobalRoles.burner], + [acDefaultAdmin.address, acDefaultAdmin.address], + acDefaultAdmin, + ); + await grantRoleViaTimelock( + ctx, + mGlobalRoles.greenlisted, + holder.address, + acDefaultAdmin, + ); + + await mint( + { tokenContract: mGlobal, owner: acDefaultAdmin }, + holder.address, + amount, + ); + + await bulkScheduleTimelockOperationTester( + ctx, + [mGlobal.address], + [ + mGlobal.interface.encodeFunctionData('burn', [ + holder.address, + amount, + ]), + ], + {}, + { + from: acDefaultAdmin, + revertCustomError: { + contract: timelockManager, + customErrorName: 'InvalidPreflightError', + }, + }, + ); + }); + }); + + describe('transfer()', () => { + it('should transfer between greenlisted token holders', async function () { + const { + mGlobal, + accessControl, + acDefaultAdmin, + mGlobalHolders, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + const from = mGlobalHolders[0]; + const to = mGlobalHolders[1]; + const amount = parseUnits('0.01'); + const greenlistRole = await mGlobal.greenlistedRole(); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)'](greenlistRole, from.address); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)'](greenlistRole, to.address); + + await expect(mGlobal.connect(from).transfer(to.address, amount)).not + .reverted; + }); + + it('should fail: when sender is not greenlisted', async () => { + const { + mGlobal, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [, from, to] = await ethers.getSigners(); + const amount = parseUnits('1'); + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + mGlobalRoles.minter, + acDefaultAdmin.address, + ); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)'](mGlobalRoles.greenlisted, to.address); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + mGlobalRoles.greenlisted, + from.address, + ); + + await mint( + { tokenContract: mGlobal, owner: acDefaultAdmin }, + from.address, + amount, + ); + + await accessControl + .connect(acDefaultAdmin) + .revokeRole(mGlobalRoles.greenlisted, from.address); + + await expect( + mGlobal.connect(from).transfer(to.address, amount), + ).revertedWithCustomError(mGlobal, 'NotGreenlisted'); + }); + }); }); - it('redeem request', async function () { - const { - xaut0Whale, - xaut0, - vaultsManager, - redemptionVaultSwapper, - xbxaut, - xbxautDataFeed, - tokenManager, - } = await loadFixture(hyperEvmUpgradeFixture); - - await mint( - { tokenContract: xbxaut, owner: tokenManager }, - xaut0Whale, - parseUnits('1'), - ); - await approveBase18(xaut0Whale, xbxaut, redemptionVaultSwapper, 1); - - await redeemRequestTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - redemptionVault: redemptionVaultSwapper, - owner: vaultsManager, - }, - xaut0, - 1, - { from: xaut0Whale }, - ); + describe('mTBILL DataFeed', () => { + describe('getDataInBase18()', () => { + it('should return a positive price', async () => { + const { mTbillDataFeed } = await loadFixture(mainnetUpgradeFixture); + + const price = await mTbillDataFeed.getDataInBase18(); + expect(price).to.be.gt(0); + }); + }); + + describe('aggregator()', () => { + it('should expose configured aggregator address', async () => { + const { mTbillDataFeed } = await loadFixture(mainnetUpgradeFixture); + + expect(await mTbillDataFeed.aggregator()).to.not.eq( + constants.AddressZero, + ); + }); + }); + + describe('changeAggregator()', () => { + it('should change aggregator after resetting delays', async () => { + const { + mTbillDataFeed, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [deployer] = await ethers.getSigners(); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + const contractAdminRole = await mTbillDataFeed.contractAdminRole(); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + contractAdminRole, + acDefaultAdmin.address, + ); + + const newAggregator = await new AggregatorV3Mock__factory( + deployer, + ).deploy(); + + await mTbillDataFeed + .connect(acDefaultAdmin) + .changeAggregator(newAggregator.address); + + expect(await mTbillDataFeed.aggregator()).to.eq(newAggregator.address); + }); + + it('should fail: when caller has no feed admin permission', async () => { + const { mTbillDataFeed } = await loadFixture(mainnetUpgradeFixture); + const [deployer, unauthorized] = await ethers.getSigners(); + + const newAggregator = await new AggregatorV3Mock__factory( + deployer, + ).deploy(); + + await expect( + mTbillDataFeed + .connect(unauthorized) + .changeAggregator(newAggregator.address), + ).revertedWithCustomError( + mTbillDataFeed, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + + it('should change aggregator via timelock', async () => { + const { + mTbillDataFeed, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [deployer] = await ethers.getSigners(); + + const ctx = { + timelockManager, + timelock, + accessControl, + owner: acDefaultAdmin, + }; + + const contractAdminRole = await mTbillDataFeed.contractAdminRole(); + await grantRoleViaTimelock( + ctx, + contractAdminRole, + acDefaultAdmin.address, + acDefaultAdmin, + ); + + const newAggregator = await new AggregatorV3Mock__factory( + deployer, + ).deploy(); + + await executeWriteViaTimelock( + ctx, + mTbillDataFeed.address, + mTbillDataFeed.interface.encodeFunctionData('changeAggregator', [ + newAggregator.address, + ]), + acDefaultAdmin, + ); + + expect(await mTbillDataFeed.aggregator()).to.eq(newAggregator.address); + }); + }); + + describe('setHealthyDiff()', () => { + const newHealthyDiff = 2 * 24 * 3600; + + it('should set healthy diff after resetting delays', async () => { + const { + mTbillDataFeed, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + const contractAdminRole = await mTbillDataFeed.contractAdminRole(); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + contractAdminRole, + acDefaultAdmin.address, + ); + + await mTbillDataFeed + .connect(acDefaultAdmin) + .setHealthyDiff(newHealthyDiff); + + expect(await mTbillDataFeed.healthyDiff()).to.eq(newHealthyDiff); + }); + + it('should set healthy diff via timelock', async () => { + const { + mTbillDataFeed, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + const ctx = { + timelockManager, + timelock, + accessControl, + owner: acDefaultAdmin, + }; + + const contractAdminRole = await mTbillDataFeed.contractAdminRole(); + await grantRoleViaTimelock( + ctx, + contractAdminRole, + acDefaultAdmin.address, + acDefaultAdmin, + ); + + await executeWriteViaTimelock( + ctx, + mTbillDataFeed.address, + mTbillDataFeed.interface.encodeFunctionData('setHealthyDiff', [ + newHealthyDiff, + ]), + acDefaultAdmin, + ); + + expect(await mTbillDataFeed.healthyDiff()).to.eq(newHealthyDiff); + }); + }); }); - it('redeem request with custom recipient', async function () { - const { - xaut0Whale, - xaut0, - vaultsManager, - redemptionVaultSwapper, - xbxaut, - xbxautDataFeed, - tokenManager, - customRecipient, - } = await loadFixture(hyperEvmUpgradeFixture); - - await mint( - { tokenContract: xbxaut, owner: tokenManager }, - xaut0Whale, - parseUnits('1'), - ); - await approveBase18(xaut0Whale, xbxaut, redemptionVaultSwapper, 1); - - await redeemRequestTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - redemptionVault: redemptionVaultSwapper, - owner: vaultsManager, - customRecipient, - }, - xaut0, - 1, - { from: xaut0Whale }, - ); + describe('mTBILL CustomAggregatorFeed', () => { + describe('latestRoundData()', () => { + it('should return valid round data', async () => { + const { mTbillCustomFeed } = await loadFixture(mainnetUpgradeFixture); + + const [, answer] = await mTbillCustomFeed.latestRoundData(); + expect(answer).to.be.gt(0); + }); + }); + + describe('decimals()', () => { + it('should return feed decimals', async () => { + const { mTbillCustomFeed } = await loadFixture(mainnetUpgradeFixture); + + expect(await mTbillCustomFeed.decimals()).to.eq(8); + }); + }); + + describe('setRoundData()', () => { + it('should submit a new round after resetting delays', async () => { + const { + mTbillCustomFeed, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + const contractAdminRole = await mTbillCustomFeed.contractAdminRole(); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + contractAdminRole, + acDefaultAdmin.address, + ); + + const roundBefore = await mTbillCustomFeed.latestRound(); + const answer = await mTbillCustomFeed.lastAnswer(); + + await mTbillCustomFeed.connect(acDefaultAdmin).setRoundData(answer); + + expect(await mTbillCustomFeed.latestRound()).to.eq(roundBefore.add(1)); + expect(await mTbillCustomFeed.lastAnswer()).to.eq(answer); + }); + + it('should fail: when caller has no feed admin permission', async () => { + const { mTbillCustomFeed } = await loadFixture(mainnetUpgradeFixture); + const [, unauthorized] = await ethers.getSigners(); + const answer = await mTbillCustomFeed.lastAnswer(); + + await expect( + mTbillCustomFeed.connect(unauthorized).setRoundData(answer), + ).revertedWithCustomError( + mTbillCustomFeed, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + + it('should submit a new round via timelock', async () => { + const { + mTbillCustomFeed, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + const ctx = { + timelockManager, + timelock, + accessControl, + owner: acDefaultAdmin, + }; + + const contractAdminRole = await mTbillCustomFeed.contractAdminRole(); + await grantRoleViaTimelock( + ctx, + contractAdminRole, + acDefaultAdmin.address, + acDefaultAdmin, + ); + + const roundBefore = await mTbillCustomFeed.latestRound(); + const answer = await mTbillCustomFeed.lastAnswer(); + + await executeWriteViaTimelock( + ctx, + mTbillCustomFeed.address, + mTbillCustomFeed.interface.encodeFunctionData('setRoundData', [ + answer, + ]), + acDefaultAdmin, + ); + + expect(await mTbillCustomFeed.latestRound()).to.eq(roundBefore.add(1)); + }); + }); + + describe('setMaxAnswerDeviation()', () => { + const newDeviation = parseUnits('1', 8); + + it('should set max answer deviation after resetting delays', async () => { + const { + mTbillCustomFeed, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + const contractAdminRole = await mTbillCustomFeed.contractAdminRole(); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + contractAdminRole, + acDefaultAdmin.address, + ); + + await mTbillCustomFeed + .connect(acDefaultAdmin) + .setMaxAnswerDeviation(newDeviation); + + expect(await mTbillCustomFeed.maxAnswerDeviation()).to.eq(newDeviation); + }); + + it('should set max answer deviation via timelock', async () => { + const { + mTbillCustomFeed, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + const ctx = { + timelockManager, + timelock, + accessControl, + owner: acDefaultAdmin, + }; + + const contractAdminRole = await mTbillCustomFeed.contractAdminRole(); + await grantRoleViaTimelock( + ctx, + contractAdminRole, + acDefaultAdmin.address, + acDefaultAdmin, + ); + + await executeWriteViaTimelock( + ctx, + mTbillCustomFeed.address, + mTbillCustomFeed.interface.encodeFunctionData( + 'setMaxAnswerDeviation', + [newDeviation], + ), + acDefaultAdmin, + ); + + expect(await mTbillCustomFeed.maxAnswerDeviation()).to.eq(newDeviation); + }); + }); }); - it('redeem request approve', async function () { - const { - xaut0Whale, - xaut0, - vaultsManager, - redemptionVaultSwapper, - xbxaut, - xbxautDataFeed, - tokenManager, - } = await loadFixture(hyperEvmUpgradeFixture); - - await mint( - { tokenContract: xbxaut, owner: tokenManager }, - xaut0Whale, - parseUnits('1'), - ); - await approveBase18(xaut0Whale, xbxaut, redemptionVaultSwapper, 1); - - const { requestId, rate } = await redeemRequestTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - redemptionVault: redemptionVaultSwapper, - owner: vaultsManager, - }, - xaut0, - 1, - { from: xaut0Whale }, - ); + describe('mGLOBAL DataFeed', () => { + describe('getDataInBase18()', () => { + it('should return a positive price', async () => { + const { mGlobalDataFeed } = await loadFixture(mainnetUpgradeFixture); - await safeApproveRedeemRequestTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - redemptionVault: redemptionVaultSwapper, - owner: vaultsManager, - }, - requestId!, - rate!, - ); + const price = await mGlobalDataFeed.getDataInBase18(); + expect(price).to.be.gt(0); + }); + }); + + describe('changeAggregator()', () => { + it('should change aggregator after resetting delays', async () => { + const { + mGlobalDataFeed, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [deployer] = await ethers.getSigners(); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + const contractAdminRole = await mGlobalDataFeed.contractAdminRole(); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + contractAdminRole, + acDefaultAdmin.address, + ); + + const newAggregator = await new AggregatorV3Mock__factory( + deployer, + ).deploy(); + + await mGlobalDataFeed + .connect(acDefaultAdmin) + .changeAggregator(newAggregator.address); + + expect(await mGlobalDataFeed.aggregator()).to.eq(newAggregator.address); + }); + + it('should fail: when caller has no feed admin permission', async () => { + const { mGlobalDataFeed } = await loadFixture(mainnetUpgradeFixture); + const [deployer, unauthorized] = await ethers.getSigners(); + + const newAggregator = await new AggregatorV3Mock__factory( + deployer, + ).deploy(); + + await expect( + mGlobalDataFeed + .connect(unauthorized) + .changeAggregator(newAggregator.address), + ).revertedWithCustomError( + mGlobalDataFeed, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + + it('should change aggregator via timelock', async () => { + const { + mGlobalDataFeed, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + const [deployer] = await ethers.getSigners(); + + const ctx = { + timelockManager, + timelock, + accessControl, + owner: acDefaultAdmin, + }; + + const contractAdminRole = await mGlobalDataFeed.contractAdminRole(); + await grantRoleViaTimelock( + ctx, + contractAdminRole, + acDefaultAdmin.address, + acDefaultAdmin, + ); + + const newAggregator = await new AggregatorV3Mock__factory( + deployer, + ).deploy(); + + await executeWriteViaTimelock( + ctx, + mGlobalDataFeed.address, + mGlobalDataFeed.interface.encodeFunctionData('changeAggregator', [ + newAggregator.address, + ]), + acDefaultAdmin, + ); + + expect(await mGlobalDataFeed.aggregator()).to.eq(newAggregator.address); + }); + }); }); - it('redeem request approve bulk', async function () { - const { - xaut0Whale, - xaut0, - vaultsManager, - redemptionVaultSwapper, - xbxaut, - xbxautDataFeed, - tokenManager, - } = await loadFixture(hyperEvmUpgradeFixture); - - await mint( - { tokenContract: xbxaut, owner: tokenManager }, - xaut0Whale, - parseUnits('1'), - ); - await approveBase18(xaut0Whale, xbxaut, redemptionVaultSwapper, 1); - - const { requestId, rate } = await redeemRequestTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - redemptionVault: redemptionVaultSwapper, - owner: vaultsManager, - }, - xaut0, - 1, - { from: xaut0Whale }, - ); + describe('mGLOBAL CustomAggregatorFeedGrowth', () => { + describe('latestRoundData()', () => { + it('should return valid round data', async () => { + const { mGlobalCustomFeedGrowth } = await loadFixture( + mainnetUpgradeFixture, + ); - await safeBulkApproveRedeemRequestTest( - { - mTBILL: xbxaut, - mTokenToUsdDataFeed: xbxautDataFeed, - redemptionVault: redemptionVaultSwapper, - owner: vaultsManager, - }, - [{ id: requestId! }], - rate!, - ); + const [, answer] = await mGlobalCustomFeedGrowth.latestRoundData(); + expect(answer).to.be.gt(0); + }); + }); + + describe('decimals()', () => { + it('should return feed decimals', async () => { + const { mGlobalCustomFeedGrowth } = await loadFixture( + mainnetUpgradeFixture, + ); + + expect(await mGlobalCustomFeedGrowth.decimals()).to.eq(8); + }); + }); + + describe('setRoundData()', () => { + it('should submit a new round after resetting delays', async () => { + const { + mGlobalCustomFeedGrowth, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + const contractAdminRole = + await mGlobalCustomFeedGrowth.contractAdminRole(); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + contractAdminRole, + acDefaultAdmin.address, + ); + + const raw = await mGlobalCustomFeedGrowth.latestRoundDataRaw(); + const roundBefore = await mGlobalCustomFeedGrowth.latestRound(); + const dataTimestamp = + (await ethers.provider.getBlock('latest')).timestamp - 1; + + await mGlobalCustomFeedGrowth + .connect(acDefaultAdmin) + .setRoundData(raw.answer, dataTimestamp, raw.growthApr); + + expect(await mGlobalCustomFeedGrowth.latestRound()).to.eq( + roundBefore.add(1), + ); + }); + + it('should fail: when caller has no feed admin permission', async () => { + const { mGlobalCustomFeedGrowth } = await loadFixture( + mainnetUpgradeFixture, + ); + const [, unauthorized] = await ethers.getSigners(); + + const raw = await mGlobalCustomFeedGrowth.latestRoundDataRaw(); + const dataTimestamp = + (await ethers.provider.getBlock('latest')).timestamp - 1; + + await expect( + mGlobalCustomFeedGrowth + .connect(unauthorized) + .setRoundData(raw.answer, dataTimestamp, raw.growthApr), + ).revertedWithCustomError( + mGlobalCustomFeedGrowth, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + + it('should submit a new round via timelock', async () => { + const { + mGlobalCustomFeedGrowth, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + const ctx = { + timelockManager, + timelock, + accessControl, + owner: acDefaultAdmin, + }; + + const contractAdminRole = + await mGlobalCustomFeedGrowth.contractAdminRole(); + await grantRoleViaTimelock( + ctx, + contractAdminRole, + acDefaultAdmin.address, + acDefaultAdmin, + ); + + const raw = await mGlobalCustomFeedGrowth.latestRoundDataRaw(); + const roundBefore = await mGlobalCustomFeedGrowth.latestRound(); + const dataTimestamp = + (await ethers.provider.getBlock('latest')).timestamp - 1; + + await executeWriteViaTimelock( + ctx, + mGlobalCustomFeedGrowth.address, + mGlobalCustomFeedGrowth.interface.encodeFunctionData('setRoundData', [ + raw.answer, + dataTimestamp, + raw.growthApr, + ]), + acDefaultAdmin, + ); + + expect(await mGlobalCustomFeedGrowth.latestRound()).to.eq( + roundBefore.add(1), + ); + }); + }); + + describe('setMaxGrowthApr()', () => { + it('should set max growth apr after resetting delays', async () => { + const { + mGlobalCustomFeedGrowth, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + const contractAdminRole = + await mGlobalCustomFeedGrowth.contractAdminRole(); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + contractAdminRole, + acDefaultAdmin.address, + ); + + const newMaxGrowthApr = await mGlobalCustomFeedGrowth.maxGrowthApr(); + + await mGlobalCustomFeedGrowth + .connect(acDefaultAdmin) + .setMaxGrowthApr(newMaxGrowthApr); + + expect(await mGlobalCustomFeedGrowth.maxGrowthApr()).to.eq( + newMaxGrowthApr, + ); + }); + + it('should set max growth apr via timelock', async () => { + const { + mGlobalCustomFeedGrowth, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + const ctx = { + timelockManager, + timelock, + accessControl, + owner: acDefaultAdmin, + }; + + const contractAdminRole = + await mGlobalCustomFeedGrowth.contractAdminRole(); + await grantRoleViaTimelock( + ctx, + contractAdminRole, + acDefaultAdmin.address, + acDefaultAdmin, + ); + + const newMaxGrowthApr = await mGlobalCustomFeedGrowth.maxGrowthApr(); + + await executeWriteViaTimelock( + ctx, + mGlobalCustomFeedGrowth.address, + mGlobalCustomFeedGrowth.interface.encodeFunctionData( + 'setMaxGrowthApr', + [newMaxGrowthApr], + ), + acDefaultAdmin, + ); + + expect(await mGlobalCustomFeedGrowth.maxGrowthApr()).to.eq( + newMaxGrowthApr, + ); + }); + }); + + describe('setOnlyUp()', () => { + it('should set onlyUp after resetting delays', async () => { + const { + mGlobalCustomFeedGrowth, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + const contractAdminRole = + await mGlobalCustomFeedGrowth.contractAdminRole(); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + contractAdminRole, + acDefaultAdmin.address, + ); + + const newOnlyUp = !(await mGlobalCustomFeedGrowth.onlyUp()); + + await mGlobalCustomFeedGrowth + .connect(acDefaultAdmin) + .setOnlyUp(newOnlyUp); + + expect(await mGlobalCustomFeedGrowth.onlyUp()).to.eq(newOnlyUp); + }); + + it('should set onlyUp via timelock', async () => { + const { + mGlobalCustomFeedGrowth, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + const ctx = { + timelockManager, + timelock, + accessControl, + owner: acDefaultAdmin, + }; + + const contractAdminRole = + await mGlobalCustomFeedGrowth.contractAdminRole(); + await grantRoleViaTimelock( + ctx, + contractAdminRole, + acDefaultAdmin.address, + acDefaultAdmin, + ); + + const newOnlyUp = !(await mGlobalCustomFeedGrowth.onlyUp()); + + await executeWriteViaTimelock( + ctx, + mGlobalCustomFeedGrowth.address, + mGlobalCustomFeedGrowth.interface.encodeFunctionData('setOnlyUp', [ + newOnlyUp, + ]), + acDefaultAdmin, + ); + + expect(await mGlobalCustomFeedGrowth.onlyUp()).to.eq(newOnlyUp); + }); + }); + + describe('setMaxAnswerDeviation()', () => { + const newDeviation = parseUnits('1', 8); + + it('should set max answer deviation after resetting delays', async () => { + const { + mGlobalCustomFeedGrowth, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + await resetTimelockDelays({ + timelockManager, + acDefaultAdmin, + accessControl, + timelock, + }); + + const contractAdminRole = + await mGlobalCustomFeedGrowth.contractAdminRole(); + await accessControl + .connect(acDefaultAdmin) + ['grantRole(bytes32,address)']( + contractAdminRole, + acDefaultAdmin.address, + ); + + await mGlobalCustomFeedGrowth + .connect(acDefaultAdmin) + .setMaxAnswerDeviation(newDeviation); + + expect(await mGlobalCustomFeedGrowth.maxAnswerDeviation()).to.eq( + newDeviation, + ); + }); + + it('should set max answer deviation via timelock', async () => { + const { + mGlobalCustomFeedGrowth, + accessControl, + acDefaultAdmin, + timelockManager, + timelock, + } = await loadFixture(mainnetUpgradeFixture); + + const ctx = { + timelockManager, + timelock, + accessControl, + owner: acDefaultAdmin, + }; + + const contractAdminRole = + await mGlobalCustomFeedGrowth.contractAdminRole(); + await grantRoleViaTimelock( + ctx, + contractAdminRole, + acDefaultAdmin.address, + acDefaultAdmin, + ); + + await executeWriteViaTimelock( + ctx, + mGlobalCustomFeedGrowth.address, + mGlobalCustomFeedGrowth.interface.encodeFunctionData( + 'setMaxAnswerDeviation', + [newDeviation], + ), + acDefaultAdmin, + ); + + expect(await mGlobalCustomFeedGrowth.maxAnswerDeviation()).to.eq( + newDeviation, + ); + }); + }); }); }); diff --git a/test/integration/DepositVaultWithUSTB.test.ts b/test/integration/DepositVaultWithUSTB.test.ts index cfa043e1..b2be35ad 100644 --- a/test/integration/DepositVaultWithUSTB.test.ts +++ b/test/integration/DepositVaultWithUSTB.test.ts @@ -112,7 +112,13 @@ describe('DepositVaultWithUSTB - Mainnet Fork Integration Tests', function () { }, usdc, usdcAmount, - { from: testUser, revertMessage: 'DVU: unsupported USTB token' }, + { + from: testUser, + revertCustomError: { + customErrorName: 'UnsupportedUSTBToken', + args: [usdc.address], + }, + }, ); }); @@ -170,7 +176,12 @@ describe('DepositVaultWithUSTB - Mainnet Fork Integration Tests', function () { }, usdc, usdcAmount, - { from: testUser, revertMessage: 'DVU: USTB fee is not 0' }, + { + from: testUser, + revertCustomError: { + customErrorName: 'USTBFeeNotZero', + }, + }, ); }); }); diff --git a/test/integration/RedemptionVaultWithAave.test.ts b/test/integration/RedemptionVaultWithAave.test.ts index cc1b6f4f..2df4cae5 100644 --- a/test/integration/RedemptionVaultWithAave.test.ts +++ b/test/integration/RedemptionVaultWithAave.test.ts @@ -179,8 +179,10 @@ describe('RedemptionVaultWithAave - Mainnet Fork Integration Tests', function () mTBILLAmount, ); - // Perform redemption: 1000 mTBILL @ 1:1 rate, 1% fee = 990 USDC needed - // Vault has 500 USDC, so shortfall = 490 USDC from Aave + // Perform redemption: 1000 mTBILL @ 1:1 rate, 1% fee. + // The vault sources the gross redeem amount (1000 USDC, fee-inclusive) and + // keeps the 10 USDC fee. With 500 USDC on hand the shortfall pulled from + // Aave is 1000 - 500 = 500 USDC. const result = await redeemInstantWithAaveTest( { redemptionVault: redemptionVaultWithAave, @@ -197,8 +199,8 @@ describe('RedemptionVaultWithAave - Mainnet Fork Integration Tests', function () // Verify user received USDC expect(result?.userUSDCReceived).to.equal(parseUnits('990', 6)); - // Verify aToken decrease equals the shortfall (990 - 500 = 490) - const expectedShortfall = parseUnits('490', 6); + // Verify aToken decrease equals the shortfall (1000 - 500 = 500) + const expectedShortfall = parseUnits('500', 6); expect(result?.aTokenUsed).to.be.closeTo( expectedShortfall, parseUnits('1', 6), // 1 USDC tolerance for Aave interest accrual @@ -250,7 +252,7 @@ describe('RedemptionVaultWithAave - Mainnet Fork Integration Tests', function () mTBILLAmount, { from: testUser, - revertMessage: 'RVA: insufficient aToken balance', + revertMessage: 'ERC20: transfer amount exceeds balance', }, ); }); @@ -309,7 +311,7 @@ describe('RedemptionVaultWithAave - Mainnet Fork Integration Tests', function () mTBILLAmount, { from: testUser, - revertMessage: 'RVA: no pool for token', + revertMessage: 'ERC20: transfer amount exceeds balance', }, ); }); diff --git a/test/integration/RedemptionVaultWithMToken.test.ts b/test/integration/RedemptionVaultWithMToken.test.ts index 53d5da7c..37f40c01 100644 --- a/test/integration/RedemptionVaultWithMToken.test.ts +++ b/test/integration/RedemptionVaultWithMToken.test.ts @@ -228,7 +228,7 @@ describe('RedemptionVaultWithMToken - Mainnet Fork Integration Tests', function ); await redemptionVaultWithMToken .connect(owner) - .withdrawToken(mTBILL.address, vaultMTBILL, owner.address); + .withdrawToken(mTBILL.address, vaultMTBILL); const mFONEAmount = 1000; @@ -252,7 +252,7 @@ describe('RedemptionVaultWithMToken - Mainnet Fork Integration Tests', function parseUnits(String(mFONEAmount)), 0, ), - ).to.be.revertedWith('RVMT: balance < needed'); + ).to.be.revertedWith('ERC20: transfer amount exceeds balance'); }); }); }); diff --git a/test/integration/RedemptionVaultWithMorpho.test.ts b/test/integration/RedemptionVaultWithMorpho.test.ts index b0b54460..eedebfe0 100644 --- a/test/integration/RedemptionVaultWithMorpho.test.ts +++ b/test/integration/RedemptionVaultWithMorpho.test.ts @@ -251,7 +251,7 @@ describe('RedemptionVaultWithMorpho - Mainnet Fork Integration Tests', function mTBILLAmount, { from: testUser, - revertMessage: 'RVM: insufficient shares', + revertMessage: 'ERC20: transfer amount exceeds balance', }, ); }); @@ -310,7 +310,7 @@ describe('RedemptionVaultWithMorpho - Mainnet Fork Integration Tests', function mTBILLAmount, { from: testUser, - revertMessage: 'RVM: no vault for token', + revertMessage: 'ERC20: transfer amount exceeds balance', }, ); }); diff --git a/test/integration/RedemptionVaultWithUSTB.test.ts b/test/integration/RedemptionVaultWithUSTB.test.ts index 0ed4f87c..eaafc3c6 100644 --- a/test/integration/RedemptionVaultWithUSTB.test.ts +++ b/test/integration/RedemptionVaultWithUSTB.test.ts @@ -208,7 +208,7 @@ describe('RedemptionVaultWithUSTB - Mainnet Fork Integration Tests', function () mTBILLAmount, { from: testUser, - revertMessage: 'RVU: ustb fee not zero', + revertMessage: 'ERC20: transfer amount exceeds balance', }, ); }); @@ -261,7 +261,7 @@ describe('RedemptionVaultWithUSTB - Mainnet Fork Integration Tests', function () mTBILLAmount, { from: testUser, - revertMessage: 'RVU: insufficient USTB balance', + revertMessage: 'ERC20: transfer amount exceeds balance', }, ); }); diff --git a/test/integration/fixtures/aave.fixture.ts b/test/integration/fixtures/aave.fixture.ts index 25402143..0ba88788 100644 --- a/test/integration/fixtures/aave.fixture.ts +++ b/test/integration/fixtures/aave.fixture.ts @@ -2,16 +2,18 @@ import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; import { rpcUrls } from '../../../config'; -import { getAllRoles } from '../../../helpers/roles'; import { - MidasAccessControlTest, - MTBILLTest, DepositVaultWithAaveTest, RedemptionVaultWithAaveTest, DataFeedTest, AggregatorV3Mock, } from '../../../typechain-types'; import { deployProxyContract } from '../../common/deploy.helpers'; +import { + getInitializerParamsDv, + getInitializerParamsRv, +} from '../../common/fixtures'; +import { setupIntegrationBase } from '../helpers/ac.helpers'; import { impersonateAndFundAccount, resetFork } from '../helpers/fork.helpers'; import { MAINNET_ADDRESSES } from '../helpers/mainnet-addresses'; @@ -20,50 +22,18 @@ const FORK_BLOCK_NUMBER = 24441000; async function setupAaveBase() { await resetFork(rpcUrls.main, FORK_BLOCK_NUMBER); - const [ + const { + accessControl, + mTBILL, owner, tokensReceiver, feeReceiver, requestRedeemer, vaultAdmin, testUser, - ] = await ethers.getSigners(); - const allRoles = getAllRoles(); - - const accessControl = await deployProxyContract( - 'MidasAccessControlTest', - [], - ); - - const mTBILL = await deployProxyContract('mTBILLTest', [ - accessControl.address, - ]); - - const rolesArray = [ - allRoles.common.defaultAdmin, - allRoles.tokenRoles.mTBILL.minter, - allRoles.tokenRoles.mTBILL.burner, - allRoles.tokenRoles.mTBILL.pauser, - allRoles.tokenRoles.mTBILL.depositVaultAdmin, - allRoles.tokenRoles.mTBILL.redemptionVaultAdmin, - allRoles.common.greenlistedOperator, - ]; - - for (const role of rolesArray) { - await accessControl.grantRole(role, owner.address); - } - - await accessControl.grantRole( - allRoles.tokenRoles.mTBILL.depositVaultAdmin, - vaultAdmin.address, - ); - - await accessControl.grantRole( - allRoles.tokenRoles.mTBILL.redemptionVaultAdmin, - vaultAdmin.address, - ); - - await accessControl.grantRole(allRoles.common.greenlisted, testUser.address); + clawbackReceiver, + roles: allRoles, + } = await setupIntegrationBase(); const usdcAggregator = (await ( await ethers.getContractFactory('AggregatorV3Mock') @@ -167,6 +137,7 @@ async function setupAaveBase() { usdtWhale, aUsdtWhale, roles: allRoles, + clawbackReceiver, }; } @@ -178,30 +149,22 @@ export async function aaveDepositFixture() { const depositVaultWithAave = await deployProxyContract( 'DepositVaultWithAaveTest', - [ - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: base.mTokenToUsdDataFeed.address, - }, - { - feeReceiver: base.feeReceiver.address, - tokensReceiver: base.tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: ethers.constants.MaxUint256, - }, - ethers.constants.AddressZero, // sanctions list - 200, - parseUnits('0'), - 0, - ethers.constants.MaxUint256, - ], + getInitializerParamsDv({ + accessControl: accessControl.address, + mockedSanctionsList: ethers.constants.AddressZero, + mTBILL: mTBILL.address, + mTokenToUsdDataFeed: base.mTokenToUsdDataFeed.address, + tokensReceiver: base.tokensReceiver.address, + minAmount: parseUnits('0'), + instantFee: 100, + variationTolerance: 200, + maxApproveRequestId: ethers.constants.MaxUint256, + }), + undefined, ); // Grant MINTER_ROLE to deposit vault - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( roles.tokenRoles.mTBILL.minter, depositVaultWithAave.address, ); @@ -247,35 +210,23 @@ export async function aaveRedemptionFixture() { const redemptionVaultWithAave = await deployProxyContract( 'RedemptionVaultWithAaveTest', - [ - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: base.mTokenToUsdDataFeed.address, - }, - { - feeReceiver: base.feeReceiver.address, - tokensReceiver: base.tokensReceiver.address, - }, - { - instantFee: 100, // 1% - instantDailyLimit: ethers.constants.MaxUint256, - }, - ethers.constants.AddressZero, // sanctions list - 200, // variation tolerance 2% - parseUnits('100', 18), // min amount - { - minFiatRedeemAmount: parseUnits('1000', 18), - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('10', 18), - }, - base.requestRedeemer.address, - ], - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address)', + getInitializerParamsRv({ + accessControl: accessControl.address, + mockedSanctionsList: ethers.constants.AddressZero, + mTBILL: mTBILL.address, + mTokenToUsdDataFeed: base.mTokenToUsdDataFeed.address, + tokensReceiver: base.tokensReceiver.address, + minAmount: parseUnits('0'), + instantFee: 100, + variationTolerance: 200, + maxApproveRequestId: ethers.constants.MaxUint256, + requestRedeemer: base.requestRedeemer.address, + }), + undefined, ); // Grant BURN_ROLE to redemption vault - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( roles.tokenRoles.mTBILL.burner, redemptionVaultWithAave.address, ); diff --git a/test/integration/fixtures/morpho.fixture.ts b/test/integration/fixtures/morpho.fixture.ts index 299984d6..99f62a73 100644 --- a/test/integration/fixtures/morpho.fixture.ts +++ b/test/integration/fixtures/morpho.fixture.ts @@ -2,16 +2,18 @@ import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; import { rpcUrls } from '../../../config'; -import { getAllRoles } from '../../../helpers/roles'; import { - MidasAccessControlTest, - MTBILLTest, DepositVaultWithMorphoTest, RedemptionVaultWithMorphoTest, DataFeedTest, AggregatorV3Mock, } from '../../../typechain-types'; import { deployProxyContract } from '../../common/deploy.helpers'; +import { + getInitializerParamsDv, + getInitializerParamsRv, +} from '../../common/fixtures'; +import { setupIntegrationBase } from '../helpers/ac.helpers'; import { impersonateAndFundAccount, resetFork } from '../helpers/fork.helpers'; import { MAINNET_ADDRESSES } from '../helpers/mainnet-addresses'; @@ -21,50 +23,17 @@ const FORK_BLOCK_NUMBER = 24441000; async function setupMorphoBase() { await resetFork(rpcUrls.main, FORK_BLOCK_NUMBER); - const [ + const { + accessControl, + mTBILL, owner, tokensReceiver, feeReceiver, requestRedeemer, vaultAdmin, testUser, - ] = await ethers.getSigners(); - const allRoles = getAllRoles(); - - const accessControl = await deployProxyContract( - 'MidasAccessControlTest', - [], - ); - - const mTBILL = await deployProxyContract('mTBILLTest', [ - accessControl.address, - ]); - - const rolesArray = [ - allRoles.common.defaultAdmin, - allRoles.tokenRoles.mTBILL.minter, - allRoles.tokenRoles.mTBILL.burner, - allRoles.tokenRoles.mTBILL.pauser, - allRoles.tokenRoles.mTBILL.depositVaultAdmin, - allRoles.tokenRoles.mTBILL.redemptionVaultAdmin, - allRoles.common.greenlistedOperator, - ]; - - for (const role of rolesArray) { - await accessControl.grantRole(role, owner.address); - } - - await accessControl.grantRole( - allRoles.tokenRoles.mTBILL.depositVaultAdmin, - vaultAdmin.address, - ); - - await accessControl.grantRole( - allRoles.tokenRoles.mTBILL.redemptionVaultAdmin, - vaultAdmin.address, - ); - - await accessControl.grantRole(allRoles.common.greenlisted, testUser.address); + roles: allRoles, + } = await setupIntegrationBase(); const usdcAggregator = (await ( await ethers.getContractFactory('AggregatorV3Mock') @@ -180,30 +149,22 @@ export async function morphoDepositFixture() { const depositVaultWithMorpho = await deployProxyContract( 'DepositVaultWithMorphoTest', - [ - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: base.mTokenToUsdDataFeed.address, - }, - { - feeReceiver: base.feeReceiver.address, - tokensReceiver: base.tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: ethers.constants.MaxUint256, - }, - ethers.constants.AddressZero, // sanctions list - 200, - parseUnits('0'), - 0, - ethers.constants.MaxUint256, - ], + getInitializerParamsDv({ + accessControl: accessControl.address, + mockedSanctionsList: ethers.constants.AddressZero, + mTBILL: mTBILL.address, + mTokenToUsdDataFeed: base.mTokenToUsdDataFeed.address, + tokensReceiver: base.tokensReceiver.address, + minAmount: parseUnits('0'), + instantFee: 100, + variationTolerance: 200, + maxApproveRequestId: ethers.constants.MaxUint256, + }), + undefined, ); // Grant MINTER_ROLE to deposit vault - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( roles.tokenRoles.mTBILL.minter, depositVaultWithMorpho.address, ); @@ -255,35 +216,23 @@ export async function morphoRedemptionFixture() { const redemptionVaultWithMorpho = await deployProxyContract( 'RedemptionVaultWithMorphoTest', - [ - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: base.mTokenToUsdDataFeed.address, - }, - { - feeReceiver: base.feeReceiver.address, - tokensReceiver: base.tokensReceiver.address, - }, - { - instantFee: 100, // 1% - instantDailyLimit: ethers.constants.MaxUint256, - }, - ethers.constants.AddressZero, // sanctions list - 200, // variation tolerance 2% - parseUnits('100', 18), // min amount - { - minFiatRedeemAmount: parseUnits('1000', 18), - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('10', 18), - }, - base.requestRedeemer.address, - ], - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address)', + getInitializerParamsRv({ + accessControl: accessControl.address, + mockedSanctionsList: ethers.constants.AddressZero, + mTBILL: mTBILL.address, + mTokenToUsdDataFeed: base.mTokenToUsdDataFeed.address, + tokensReceiver: base.tokensReceiver.address, + minAmount: parseUnits('100', 18), + instantFee: 100, + variationTolerance: 200, + maxApproveRequestId: ethers.constants.MaxUint256, + requestRedeemer: base.requestRedeemer.address, + }), + undefined, ); // Grant BURN_ROLE to redemption vault - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( roles.tokenRoles.mTBILL.burner, redemptionVaultWithMorpho.address, ); diff --git a/test/integration/fixtures/mtoken.fixture.ts b/test/integration/fixtures/mtoken.fixture.ts index 329aa6f9..c8f597f9 100644 --- a/test/integration/fixtures/mtoken.fixture.ts +++ b/test/integration/fixtures/mtoken.fixture.ts @@ -2,10 +2,7 @@ import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; import { rpcUrls } from '../../../config'; -import { getAllRoles } from '../../../helpers/roles'; import { - MidasAccessControlTest, - MTBILLTest, DepositVaultTest, DepositVaultWithMTokenTest, RedemptionVaultTest, @@ -14,6 +11,20 @@ import { AggregatorV3Mock, } from '../../../typechain-types'; import { deployProxyContract } from '../../common/deploy.helpers'; +import { + getInitializerParamsDv, + getInitializerParamsDvWithMToken, + getInitializerParamsRv, + getInitializerParamsRvWithMToken, +} from '../../common/fixtures'; +import { + DV_MTOKEN_INIT_FN, + RV_MTOKEN_INIT_FN, +} from '../../common/vault-initializer.helpers'; +import { + deployIntegrationMToken, + setupIntegrationBase, +} from '../helpers/ac.helpers'; import { impersonateAndFundAccount, resetFork } from '../helpers/fork.helpers'; import { MAINNET_ADDRESSES } from '../helpers/mainnet-addresses'; @@ -22,58 +33,33 @@ const FORK_BLOCK_NUMBER = 24441000; async function setupMTokenBase() { await resetFork(rpcUrls.main, FORK_BLOCK_NUMBER); - const [ + const { + accessControl, + mTBILL, owner, tokensReceiver, feeReceiver, requestRedeemer, vaultAdmin, testUser, - targetTokensReceiver, - ] = await ethers.getSigners(); - const allRoles = getAllRoles(); - - const accessControl = await deployProxyContract( - 'MidasAccessControlTest', - [], - ); + clawbackReceiver, + regularUsers, + roles: allRoles, + } = await setupIntegrationBase(); - // "Target" mToken (simulating mTBILL) - const mTBILL = await deployProxyContract('mTBILLTest', [ - accessControl.address, - ]); + // Extra receiver so USDC flowing through the target vault doesn't contaminate + // the product vault's tokensReceiver balance assertions. + const targetTokensReceiver = regularUsers[0]; - // "Product" mToken (simulating mFONE) - const mFONE = await deployProxyContract('mTBILLTest', [ + // "Product" mToken (simulating mFONE). Shares the mTBILL role hashes so the + // product vaults can mint/burn it with the same granted roles. + const mFONE = await deployIntegrationMToken( accessControl.address, - ]); - - const rolesArray = [ - allRoles.common.defaultAdmin, - allRoles.tokenRoles.mTBILL.minter, - allRoles.tokenRoles.mTBILL.burner, - allRoles.tokenRoles.mTBILL.pauser, - allRoles.tokenRoles.mTBILL.depositVaultAdmin, - allRoles.tokenRoles.mTBILL.redemptionVaultAdmin, - allRoles.common.greenlistedOperator, - ]; - - for (const role of rolesArray) { - await accessControl.grantRole(role, owner.address); - } - - await accessControl.grantRole( - allRoles.tokenRoles.mTBILL.depositVaultAdmin, - vaultAdmin.address, - ); - - await accessControl.grantRole( - allRoles.tokenRoles.mTBILL.redemptionVaultAdmin, - vaultAdmin.address, + clawbackReceiver.address, + allRoles.tokenRoles.mTBILL, + { name: 'mFONE', symbol: 'mFONE' }, ); - await accessControl.grantRole(allRoles.common.greenlisted, testUser.address); - // USDC data feed const usdcAggregator = (await ( await ethers.getContractFactory('AggregatorV3Mock') @@ -171,30 +157,22 @@ export async function mTokenDepositFixture() { // doesn't contaminate the product DV's tokensReceiver balance assertions const targetDepositVault = await deployProxyContract( 'DepositVaultTest', - [ - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: base.mTokenToUsdDataFeed.address, - }, - { - feeReceiver: base.feeReceiver.address, - tokensReceiver: base.targetTokensReceiver.address, - }, - { - instantFee: 0, - instantDailyLimit: ethers.constants.MaxUint256, - }, - ethers.constants.AddressZero, - 200, - parseUnits('0'), - 0, - ethers.constants.MaxUint256, - ], + getInitializerParamsDv({ + accessControl: accessControl.address, + mockedSanctionsList: ethers.constants.AddressZero, + mTBILL: mTBILL.address, + mTokenToUsdDataFeed: base.mTokenToUsdDataFeed.address, + tokensReceiver: base.targetTokensReceiver.address, + minAmount: parseUnits('0'), + instantFee: 0, + variationTolerance: 200, + maxApproveRequestId: ethers.constants.MaxUint256, + }), + undefined, ); // Grant minter to target DV (so it can mint mTBILL) - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( roles.tokenRoles.mTBILL.minter, targetDepositVault.address, ); @@ -214,38 +192,29 @@ export async function mTokenDepositFixture() { const depositVaultWithMToken = await deployProxyContract( 'DepositVaultWithMTokenTest', - [ - accessControl.address, - { - mToken: mFONE.address, - mTokenDataFeed: base.mFoneToUsdDataFeed.address, - }, - { - feeReceiver: base.feeReceiver.address, - tokensReceiver: base.tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: ethers.constants.MaxUint256, - }, - ethers.constants.AddressZero, - 200, - parseUnits('0'), - 0, - ethers.constants.MaxUint256, - targetDepositVault.address, - ], - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,uint256,uint256,address)', + getInitializerParamsDvWithMToken({ + accessControl: accessControl.address, + mockedSanctionsList: ethers.constants.AddressZero, + mTBILL: mFONE.address, + mTokenToUsdDataFeed: base.mFoneToUsdDataFeed.address, + tokensReceiver: base.tokensReceiver.address, + minAmount: parseUnits('0'), + instantFee: 100, + variationTolerance: 200, + maxApproveRequestId: ethers.constants.MaxUint256, + depositVault: targetDepositVault.address, + }), + DV_MTOKEN_INIT_FN, ); // Grant minter to product DV (so it can mint mFONE) - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( roles.tokenRoles.mTBILL.minter, depositVaultWithMToken.address, ); // Greenlist the product DV so it can call depositInstant on target DV - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( roles.common.greenlisted, depositVaultWithMToken.address, ); @@ -253,7 +222,7 @@ export async function mTokenDepositFixture() { // Waive fees on target DV for the product DV (required for _autoInvest) await targetDepositVault .connect(owner) - .addWaivedFeeAccount(depositVaultWithMToken.address); + .setWaivedFeeAccount(depositVaultWithMToken.address, true); // Add USDC as payment token on product DV await depositVaultWithMToken @@ -280,34 +249,23 @@ export async function mTokenRedemptionFixture() { // Deploy target RV (plain RedemptionVault for mTBILL) const targetRedemptionVault = await deployProxyContract( 'RedemptionVaultTest', - [ - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: base.mTokenToUsdDataFeed.address, - }, - { - feeReceiver: base.feeReceiver.address, - tokensReceiver: base.tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: ethers.constants.MaxUint256, - }, - ethers.constants.AddressZero, - 200, - parseUnits('100', 18), - { - minFiatRedeemAmount: parseUnits('1000', 18), - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('10', 18), - }, - base.requestRedeemer.address, - ], + getInitializerParamsRv({ + accessControl: accessControl.address, + mockedSanctionsList: ethers.constants.AddressZero, + mTBILL: mTBILL.address, + mTokenToUsdDataFeed: base.mTokenToUsdDataFeed.address, + tokensReceiver: base.tokensReceiver.address, + minAmount: parseUnits('100', 18), + instantFee: 100, + variationTolerance: 200, + maxApproveRequestId: ethers.constants.MaxUint256, + requestRedeemer: base.requestRedeemer.address, + }), + undefined, ); // Grant BURN_ROLE to target RV (so it can burn mTBILL) - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( roles.tokenRoles.mTBILL.burner, targetRedemptionVault.address, ); @@ -332,36 +290,24 @@ export async function mTokenRedemptionFixture() { const redemptionVaultWithMToken = await deployProxyContract( 'RedemptionVaultWithMTokenTest', - [ - accessControl.address, - { - mToken: mFONE.address, - mTokenDataFeed: base.mFoneToUsdDataFeed.address, - }, - { - feeReceiver: base.feeReceiver.address, - tokensReceiver: base.tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: ethers.constants.MaxUint256, - }, - ethers.constants.AddressZero, - 200, - parseUnits('100', 18), - { - minFiatRedeemAmount: parseUnits('1000', 18), - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('10', 18), - }, - base.requestRedeemer.address, - targetRedemptionVault.address, - ], - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address)', + getInitializerParamsRvWithMToken({ + accessControl: accessControl.address, + mockedSanctionsList: ethers.constants.AddressZero, + mTBILL: mFONE.address, + mTokenToUsdDataFeed: base.mFoneToUsdDataFeed.address, + tokensReceiver: base.tokensReceiver.address, + minAmount: parseUnits('100', 18), + instantFee: 100, + variationTolerance: 200, + maxApproveRequestId: ethers.constants.MaxUint256, + requestRedeemer: base.requestRedeemer.address, + redemptionVault: targetRedemptionVault.address, + }), + RV_MTOKEN_INIT_FN, ); // Grant BURN_ROLE to product RV (so it can burn mFONE) - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( roles.tokenRoles.mTBILL.burner, redemptionVaultWithMToken.address, ); @@ -378,7 +324,7 @@ export async function mTokenRedemptionFixture() { ); // Greenlist the product RV on access control (target RV requires greenlist) - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( roles.common.greenlisted, redemptionVaultWithMToken.address, ); @@ -386,10 +332,13 @@ export async function mTokenRedemptionFixture() { // Waive fees on target RV for the product RV address await targetRedemptionVault .connect(owner) - .addWaivedFeeAccount(redemptionVaultWithMToken.address); + .setWaivedFeeAccount(redemptionVaultWithMToken.address, true); // Mint mTBILL to product RV (simulating Fordefi deposit) - await accessControl.grantRole(roles.tokenRoles.mTBILL.minter, owner.address); + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.minter, + owner.address, + ); await mTBILL.mint(redemptionVaultWithMToken.address, parseUnits('50000')); return { diff --git a/test/integration/fixtures/pyth.fixture.ts b/test/integration/fixtures/pyth.fixture.ts index 2a6d3097..256f0cac 100644 --- a/test/integration/fixtures/pyth.fixture.ts +++ b/test/integration/fixtures/pyth.fixture.ts @@ -44,7 +44,7 @@ export async function pythAdapterFixture( ); // Grant default admin role to deployer - await midasAccessControl.grantRole( + await midasAccessControl['grantRole(bytes32,address)']( allRoles.common.defaultAdmin, deployer.address, ); diff --git a/test/integration/fixtures/upgrades.fixture.ts b/test/integration/fixtures/upgrades.fixture.ts index f84a1580..b8619563 100644 --- a/test/integration/fixtures/upgrades.fixture.ts +++ b/test/integration/fixtures/upgrades.fixture.ts @@ -1,26 +1,61 @@ -import { mine } from '@nomicfoundation/hardhat-network-helpers'; -import { parseUnits } from 'ethers/lib/utils'; +import { mine, setBalance } from '@nomicfoundation/hardhat-network-helpers'; +import { ContractFactory } from 'ethers'; +import { parseUnits } from 'ethers/lib.esm/utils'; import { ethers } from 'hardhat'; +import hre from 'hardhat'; import { rpcUrls } from '../../../config'; -import { MToken } from '../../../typechain-types'; +import { getAllRoles } from '../../../helpers/roles'; +import { + MidasAccessControl, + MidasAccessControlTimelockController, + MidasPauseManager, + MidasTimelockManager, + MidasAccessControl__factory, + DataFeed__factory, + CustomAggregatorV3CompatibleFeedGrowth__factory, + MToken, + DataFeed, + CustomAggregatorV3CompatibleFeed, + CustomAggregatorV3CompatibleFeed__factory, + MToken__factory, + CustomAggregatorV3CompatibleFeedGrowth, + MTokenPermissioned, + MTokenPermissioned__factory, +} from '../../../typechain-types'; +import { NO_DELAY, NULL_DELAY } from '../../common/ac.helpers'; +import { asyncForEach, Constructor } from '../../common/common.helpers'; +import { deployProxyContract } from '../../common/deploy.helpers'; import { impersonateAndFundAccount, resetFork } from '../helpers/fork.helpers'; -export async function hyperEvmUpgradeFixture() { - const dvProxyAddress = '0x48fb106Ef0c0C1a19EdDC9C5d27A945E66DA1C4E'; - const rvSwapperProxyAddress = '0xD26bB9B45140D17eF14FbD4fCa8Cf0d610ac50E7'; +export async function mainnetUpgradeFixture() { + const acDefaultAdminAddress = '0xd4195CF4df289a4748C1A7B6dDBE770e27bA1227'; - const newDvImplementationAddress = - '0x448897fEc88D145E22cA8594F1a928C72e1De8a6'; - const newRvSwapperImplementationAddress = - '0x67581417D7AFe1E02d1Da4AbfD4fa6a2774e625f'; - - const tokenManagerAddress = '0x46a12DDCA8c92742251b2a2c33610BF8Ae090cd9'; - const vaultsManagerAddress = '0x2ACB4BdCbEf02f81BF713b696Ac26390d7f79A12'; + const acAddress = '0x0312A9D1Ff2372DDEdCBB21e4B6389aFc919aC4B'; const proxyAdminAddress = '0xbf25b58cB8DfaD688F7BcB2b87D71C23A6600AaC'; - const [customRecipient] = await ethers.getSigners(); - await resetFork(rpcUrls.hyperevm, 9874404); + // mGLOBAL addresses + const mGlobalAddress = '0x7433806912Eae67919e66aea853d46Fa0aef98A8'; + const mGlobalCustomFeedGrowthAddress = + '0x66Aa9fcD63DF74e1f67A9452E6E59Fbc67f75E38'; + const mGlobalDataFeedAddress = '0x58476f452df10E6Bf17dc1fee418E98dE9e14868'; + + // mTBILL addresses + const mTbillDataFeedAddress = '0xfCEE9754E8C375e145303b7cE7BEca3201734A2B'; + const mTbillCustomFeedAddress = '0x056339C044055819E8Db84E71f5f2E1F536b2E5b'; + const mTbillAddress = '0xDD629E5241CbC5919847783e6C96B2De4754e438'; + + // mBTC addresses + const mBtcDataFeedAddress = '0x9987BE0c1dc5Cd284a4D766f4B5feB4F3cb3E28e'; + const mBtcCustomFeedAddress = '0xA537EF0343e83761ED42B8E017a1e495c9a189Ee'; + const mBtcAddress = '0x007115416AB6c266329a03B09a8aa39aC2eF7d9d'; + + // mROX addresses + const mRoxAddress = '0x67E1F506B148d0Fc95a4E3fFb49068ceB6855c05'; + + const [clawbackReceiver, ...signers] = await ethers.getSigners(); + + await resetFork(rpcUrls.main, 25193577); await mine(); @@ -34,92 +69,256 @@ export async function hyperEvmUpgradeFixture() { proxyAdminOwnerAddress, ); - const xaut0 = await ethers.getContractAt( - 'ERC20', - '0xf4D9235269a96aaDaFc9aDAe454a0618eBE37949', - ); + const acDefaultAdmin = await impersonateAndFundAccount(acDefaultAdminAddress); - const xaut0Whale = await impersonateAndFundAccount( - '0xaF7FD67AE6B3E25F83291D5600fBe3B776EEa4d3', - ); + await setBalance(proxyAdminOwner.address, parseUnits('1000', 18)); + await setBalance(acDefaultAdmin.address, parseUnits('1000', 18)); - const vaultsManager = await impersonateAndFundAccount(vaultsManagerAddress); + const accessControl = (await ethers.getContractAt( + 'MidasAccessControl', + acAddress, + )) as MidasAccessControl; - const tokenManager = await impersonateAndFundAccount(tokenManagerAddress); + const allRoles = getAllRoles(); + const addressesMap: Record< + string, + { + proxy: string; + implementation: Constructor; + constructorArgs?: unknown[]; + reinitializerParams?: { + fn: string; + args: unknown[]; + }; + }[] + > = { + ac: [ + { + proxy: acAddress, + implementation: MidasAccessControl__factory, + reinitializerParams: { + fn: 'initializeV2', + args: [NULL_DELAY, [allRoles.tokenRoles.mGLOBAL.greenlisted]], + }, + }, + ], + mTbill: [ + // 1 gap on the token level (mTBILL), missing gaps in Blacklistable and WithMidasAccessControl + { + proxy: mTbillAddress, + implementation: MToken__factory, + constructorArgs: [ + allRoles.tokenRoles.mTBILL.tokenManager, + allRoles.tokenRoles.mTBILL.minter, + allRoles.tokenRoles.mTBILL.burner, + ], + reinitializerParams: { + fn: 'initializeV2', + args: [clawbackReceiver.address], + }, + }, + // no gap in the product contract (MBtcDataFeed), has gap in DataFeed + { + proxy: mTbillDataFeedAddress, + implementation: DataFeed__factory, + constructorArgs: [allRoles.tokenRoles.mTBILL.customFeedAdmin], + }, + // has gap in the product contract (MBtcCustomAggregatorFeed) and no gap in CustomAggregatorV3CompatibleFeed + { + proxy: mTbillCustomFeedAddress, + implementation: CustomAggregatorV3CompatibleFeed__factory, + constructorArgs: [allRoles.tokenRoles.mTBILL.customFeedAdmin], + }, + ], + mBtc: [ + // inherits mTBILL, has 2 __gap on token level (mToken, mBTC) + { + proxy: mBtcAddress, + implementation: MToken__factory, + constructorArgs: [ + allRoles.tokenRoles.mBTC.tokenManager, + allRoles.tokenRoles.mBTC.minter, + allRoles.tokenRoles.mBTC.burner, + ], + reinitializerParams: { + fn: 'initializeV2', + args: [clawbackReceiver.address], + }, + }, + // no gap in the product contract (MBtcDataFeed), has gap in DataFeed + { + proxy: mBtcDataFeedAddress, + implementation: DataFeed__factory, + constructorArgs: [allRoles.tokenRoles.mBTC.customFeedAdmin], + }, + // no gap in the product contract (MBtcCustomAggregatorFeed) and no gap in CustomAggregatorV3CompatibleFeed + { + proxy: mBtcCustomFeedAddress, + implementation: CustomAggregatorV3CompatibleFeed__factory, + constructorArgs: [allRoles.tokenRoles.mBTC.customFeedAdmin], + }, + ], + mGlobal: [ + // inherits mTokenPermissioned, has 3 __gap on the token level (mToken, mTokenPermissioned, mGLOBAL) + { + proxy: mGlobalAddress, + implementation: MTokenPermissioned__factory, + constructorArgs: [ + allRoles.tokenRoles.mGLOBAL.tokenManager, + allRoles.tokenRoles.mGLOBAL.minter, + allRoles.tokenRoles.mGLOBAL.burner, + allRoles.tokenRoles.mGLOBAL.greenlisted, + ], + reinitializerParams: { + fn: 'initializeV2', + args: [clawbackReceiver.address], + }, + }, + { + // has gap in the product contract (MGlobalDataFeed), has gap in DataFeed + proxy: mGlobalDataFeedAddress, + implementation: DataFeed__factory, + constructorArgs: [allRoles.tokenRoles.mGLOBAL.customFeedAdmin], + }, + // has gap in the product contract (MGlobalCustomFeedGrowth), has gap in CustomAggregatorV3CompatibleFeedGrowth + { + proxy: mGlobalCustomFeedGrowthAddress, + implementation: CustomAggregatorV3CompatibleFeedGrowth__factory, + constructorArgs: [allRoles.tokenRoles.mGLOBAL.customFeedAdmin], + }, + ], + mRox: [ + // inherits mToken, has 2 __gap on the token level (mToken, mROX) + { + proxy: mRoxAddress, + implementation: MToken__factory, + constructorArgs: [ + allRoles.tokenRoles.mROX.tokenManager, + allRoles.tokenRoles.mROX.minter, + allRoles.tokenRoles.mROX.burner, + ], + reinitializerParams: { + fn: 'initializeV2', + args: [clawbackReceiver.address], + }, + }, + ], + }; - await proxyAdmin - .connect(proxyAdminOwner) - .upgrade(dvProxyAddress, newDvImplementationAddress); + await asyncForEach(Object.entries(addressesMap), async ([, values]) => { + await asyncForEach( + values, + async (val) => { + await hre.upgrades.upgradeProxy( + val.proxy, + new val.implementation(proxyAdminOwner), + { + constructorArgs: val.constructorArgs ?? [], + call: val.reinitializerParams + ? { + fn: val.reinitializerParams.fn, + args: val.reinitializerParams.args, + } + : undefined, + }, + ); + }, + true, + ); + }); - await proxyAdmin - .connect(proxyAdminOwner) - .upgrade(rvSwapperProxyAddress, newRvSwapperImplementationAddress); + const securityCouncilMembers = [ + signers[0], + signers[1], + signers[2], + signers[3], + signers[4], + ]; - const depositVault = await ethers.getContractAt( - 'HBXautDepositVault', - dvProxyAddress, + const pauseManager = await deployProxyContract( + 'MidasPauseManager', + [acAddress, NO_DELAY, 3600], ); - const redemptionVaultSwapper = await ethers.getContractAt( - 'HBXautRedemptionVaultWithSwapper', - rvSwapperProxyAddress, + const timelockManager = await deployProxyContract( + 'MidasTimelockManager', + [acAddress, 100, securityCouncilMembers.map((s) => s.address)], ); - const xbxautDataFeed = await ethers.getContractAt( - 'HBXautDataFeed', - await depositVault.mTokenDataFeed(), - ); + const timelock = + await deployProxyContract( + 'MidasAccessControlTimelockController', + [timelockManager.address], + ); - const xbxaut = (await ethers.getContractAt( - 'hbXAUt', - await depositVault.mToken(), - )) as MToken; + await accessControl + .connect(acDefaultAdmin) + .initializeRelationships(timelockManager.address, pauseManager.address); - const xaut0DataFeed = await ethers.getContractAt( - 'DataFeed', - ( - await depositVault.tokensConfig(xaut0.address) - ).dataFeed, - ); + await timelockManager + .connect(acDefaultAdmin) + .initializeTimelock(timelock.address); - await xaut0 - .connect(xaut0Whale) - .transfer(redemptionVaultSwapper.address, parseUnits('10', 6)); - - const requestRedeemerAddress = await redemptionVaultSwapper.requestRedeemer(); + const mTbill = (await ethers.getContractAt( + 'mToken', + mTbillAddress, + )) as MToken; + const mGlobal = (await ethers.getContractAt( + 'mTokenPermissioned', + mGlobalAddress, + )) as MTokenPermissioned; + const mTbillDataFeed = (await ethers.getContractAt( + 'DataFeed', + mTbillDataFeedAddress, + )) as DataFeed; + const mTbillCustomFeed = (await ethers.getContractAt( + 'CustomAggregatorV3CompatibleFeed', + mTbillCustomFeedAddress, + )) as CustomAggregatorV3CompatibleFeed; + const mGlobalDataFeed = (await ethers.getContractAt( + 'DataFeed', + mGlobalDataFeedAddress, + )) as DataFeed; + const mGlobalCustomFeedGrowth = (await ethers.getContractAt( + 'CustomAggregatorV3CompatibleFeedGrowth', + mGlobalCustomFeedGrowthAddress, + )) as CustomAggregatorV3CompatibleFeedGrowth; - const requestRedeemer = await impersonateAndFundAccount( - requestRedeemerAddress, + const mTbillHolders = await Promise.all( + [ + '0x0461bD693caE49bE9d030E5c212e080F9c78B846', + '0xc0C4Ab1D389F9540A50D1188226D7384a68cE788', + ].map((address) => impersonateAndFundAccount(address)), ); - await xaut0 - .connect(requestRedeemer) - .approve(redemptionVaultSwapper.address, parseUnits('1000', 6)); - - await xaut0 - .connect(xaut0Whale) - .transfer(requestRedeemerAddress, parseUnits('10', 6)); + const mGlobalHolders = await Promise.all( + [ + '0x882C825405fBBE45DCc1ad52b639aFbC4592EDb7', + '0xaB05c0DB9D26e96A9dcEDCAFCA23341316F6fe6F', + ].map((address) => impersonateAndFundAccount(address)), + ); return { proxyAdmin, proxyAdminOwner, - dvProxyAddress, - rvSwapperProxyAddress, - newDvImplementationAddress, - newRvSwapperImplementationAddress, - xaut0Whale, - xaut0, - vaultsManager, - redemptionVaultSwapper, - depositVault, - xbxautDataFeed, - xaut0DataFeed, - xbxaut, - tokenManager, - customRecipient, + acDefaultAdmin, + accessControl, + pauseManager, + timelockManager, + timelock, + securityCouncilMembers, + mTbill, + mGlobal, + mTbillDataFeed, + mTbillCustomFeed, + mGlobalDataFeed, + mGlobalCustomFeedGrowth, + mTbillHolders, + mGlobalHolders, + clawbackReceiver, }; } -export type DeployedContracts = Awaited< - ReturnType +export type MainnetUpgradeFixture = Awaited< + ReturnType >; diff --git a/test/integration/fixtures/ustb.fixture.ts b/test/integration/fixtures/ustb.fixture.ts index 3ee3b96f..cf10ce59 100644 --- a/test/integration/fixtures/ustb.fixture.ts +++ b/test/integration/fixtures/ustb.fixture.ts @@ -2,16 +2,22 @@ import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; import { rpcUrls } from '../../../config'; -import { getAllRoles } from '../../../helpers/roles'; import { - MidasAccessControlTest, - MTBILLTest, RedemptionVaultWithUSTBTest, DataFeedTest, AggregatorV3Mock, DepositVaultWithUSTBTest, } from '../../../typechain-types'; import { deployProxyContract } from '../../common/deploy.helpers'; +import { + getInitializerParamsDvWithUstb, + getInitializerParamsRvWithUstb, +} from '../../common/fixtures'; +import { + DV_USTB_INIT_FN, + RV_USTB_INIT_FN, +} from '../../common/vault-initializer.helpers'; +import { setupIntegrationBase } from '../helpers/ac.helpers'; import { impersonateAndFundAccount, resetFork } from '../helpers/fork.helpers'; import { MAINNET_ADDRESSES } from '../helpers/mainnet-addresses'; import { setupUSTBAllowlist } from '../helpers/ustb-helpers'; @@ -22,50 +28,17 @@ const FORK_BLOCK_NUMBER = 22540000; async function setupUstbBase() { await resetFork(rpcUrls.main, FORK_BLOCK_NUMBER); - const [ + const { + accessControl, + mTBILL, owner, tokensReceiver, feeReceiver, requestRedeemer, vaultAdmin, testUser, - ] = await ethers.getSigners(); - const allRoles = getAllRoles(); - - const accessControl = await deployProxyContract( - 'MidasAccessControlTest', - [], - ); - - const mTBILL = await deployProxyContract('mTBILLTest', [ - accessControl.address, - ]); - - const rolesArray = [ - allRoles.common.defaultAdmin, - allRoles.tokenRoles.mTBILL.minter, - allRoles.tokenRoles.mTBILL.burner, - allRoles.tokenRoles.mTBILL.pauser, - allRoles.tokenRoles.mTBILL.redemptionVaultAdmin, - allRoles.tokenRoles.mTBILL.depositVaultAdmin, - allRoles.common.greenlistedOperator, - ]; - - for (const role of rolesArray) { - await accessControl.grantRole(role, owner.address); - } - - await accessControl.grantRole( - allRoles.tokenRoles.mTBILL.redemptionVaultAdmin, - vaultAdmin.address, - ); - - await accessControl.grantRole( - allRoles.tokenRoles.mTBILL.depositVaultAdmin, - vaultAdmin.address, - ); - - await accessControl.grantRole(allRoles.common.greenlisted, testUser.address); + roles: allRoles, + } = await setupIntegrationBase(); const usdcAggregator = (await ( await ethers.getContractFactory('AggregatorV3Mock') @@ -162,32 +135,23 @@ export async function ustbDepositFixture() { const depositVaultWithUSTB = await deployProxyContract( 'DepositVaultWithUSTBTest', - [ - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: base.mTokenToUsdDataFeed.address, - }, - { - feeReceiver: base.feeReceiver.address, - tokensReceiver: base.tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: ethers.constants.MaxUint256, - }, - ethers.constants.AddressZero, // sanctions list - 200, - parseUnits('0'), - 0, - ethers.constants.MaxUint256, - MAINNET_ADDRESSES.SUPERSTATE_TOKEN_PROXY, - ], - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,uint256,uint256,address)', + getInitializerParamsDvWithUstb({ + accessControl: accessControl.address, + mockedSanctionsList: ethers.constants.AddressZero, + mTBILL: mTBILL.address, + mTokenToUsdDataFeed: base.mTokenToUsdDataFeed.address, + tokensReceiver: base.tokensReceiver.address, + minAmount: parseUnits('0'), + instantFee: 100, + variationTolerance: 200, + maxApproveRequestId: ethers.constants.MaxUint256, + ustbToken: MAINNET_ADDRESSES.SUPERSTATE_TOKEN_PROXY, + }), + DV_USTB_INIT_FN, ); // Grant MINTER_ROLE to vault - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( roles.tokenRoles.mTBILL.minter, depositVaultWithUSTB.address, ); @@ -218,36 +182,24 @@ export async function ustbRedemptionFixture() { const redemptionVaultWithUSTB = await deployProxyContract( 'RedemptionVaultWithUSTBTest', - [ - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: base.mTokenToUsdDataFeed.address, - }, - { - feeReceiver: base.feeReceiver.address, - tokensReceiver: base.tokensReceiver.address, - }, - { - instantFee: 100, // 1% - instantDailyLimit: ethers.constants.MaxUint256, - }, - ethers.constants.AddressZero, // sanctions list - 200, // variation tolerance 2% - parseUnits('100', 18), // min amount - { - minFiatRedeemAmount: parseUnits('1000', 18), - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('10', 18), - }, - base.requestRedeemer.address, - MAINNET_ADDRESSES.REDEMPTION_IDLE_PROXY, - ], - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address)', + getInitializerParamsRvWithUstb({ + accessControl: accessControl.address, + mockedSanctionsList: ethers.constants.AddressZero, + mTBILL: mTBILL.address, + mTokenToUsdDataFeed: base.mTokenToUsdDataFeed.address, + tokensReceiver: base.tokensReceiver.address, + minAmount: parseUnits('100', 18), + instantFee: 100, + variationTolerance: 200, + maxApproveRequestId: ethers.constants.MaxUint256, + requestRedeemer: base.requestRedeemer.address, + ustbRedemption: MAINNET_ADDRESSES.REDEMPTION_IDLE_PROXY, + }), + RV_USTB_INIT_FN, ); // Grant BURN_ROLE to vault - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( roles.tokenRoles.mTBILL.burner, redemptionVaultWithUSTB.address, ); diff --git a/test/integration/helpers/ac.helpers.ts b/test/integration/helpers/ac.helpers.ts new file mode 100644 index 00000000..780bbc63 --- /dev/null +++ b/test/integration/helpers/ac.helpers.ts @@ -0,0 +1,143 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { ethers } from 'hardhat'; + +import { mTokensMetadata } from '../../../helpers/mtokens-metadata'; +import { getAllRoles } from '../../../helpers/roles'; +import { + MidasAccessControlTest, + MidasAccessControlTimelockController, + MidasPauseManagerTest, + MidasTimelockManager, + MToken, +} from '../../../typechain-types'; +import { asyncForEach } from '../../common/common.helpers'; +import { deployProxyContract } from '../../common/deploy.helpers'; + +type MTokenRoles = { + minter: string; + burner: string; + tokenManager: string; +}; + +export async function deployAccessControlInfra( + councilMembers: SignerWithAddress[], +) { + const accessControl = await deployProxyContract( + 'MidasAccessControlTest', + [0, []], + ); + + const pauseManager = await deployProxyContract( + 'MidasPauseManagerTest', + [accessControl.address, 0, 3600], + ); + + const timelockManager = await deployProxyContract( + 'MidasTimelockManager', + [ + accessControl.address, + 100, + councilMembers.map((member) => member.address), + ], + ); + + const timelock = + await deployProxyContract( + 'MidasAccessControlTimelockController', + [timelockManager.address], + ); + + await timelockManager.initializeTimelock(timelock.address); + await accessControl.initializeRelationships( + timelockManager.address, + pauseManager.address, + ); + + return { accessControl, pauseManager, timelockManager, timelock }; +} + +export async function deployIntegrationMToken( + accessControl: string, + clawbackReceiver: string, + roles: MTokenRoles, + metadata: { name: string; symbol: string } = mTokensMetadata.mTBILL, +) { + return deployProxyContract( + 'mToken', + [accessControl, clawbackReceiver, metadata.name, metadata.symbol], + undefined, + [roles.tokenManager, roles.minter, roles.burner], + ); +} + +export async function setupIntegrationBase() { + const [ + owner, + tokensReceiver, + feeReceiver, + requestRedeemer, + vaultAdmin, + testUser, + clawbackReceiver, + ...rest + ] = await ethers.getSigners(); + const councilMembers = rest.slice(0, 5); + const regularUsers = rest.slice(5); + + const roles = getAllRoles(); + const mTBILLRoles = roles.tokenRoles.mTBILL; + + const { accessControl, pauseManager, timelockManager, timelock } = + await deployAccessControlInfra(councilMembers); + + const mTBILL = await deployIntegrationMToken( + accessControl.address, + clawbackReceiver.address, + mTBILLRoles, + ); + + const ownerRoles = [ + roles.common.defaultAdmin, + mTBILLRoles.minter, + mTBILLRoles.burner, + mTBILLRoles.tokenManager, + mTBILLRoles.depositVaultAdmin, + mTBILLRoles.redemptionVaultAdmin, + roles.common.greenlistedOperator, + ]; + + await asyncForEach(ownerRoles, async (role) => { + await accessControl['grantRole(bytes32,address)'](role, owner.address); + }); + + await accessControl['grantRole(bytes32,address)']( + mTBILLRoles.depositVaultAdmin, + vaultAdmin.address, + ); + await accessControl['grantRole(bytes32,address)']( + mTBILLRoles.redemptionVaultAdmin, + vaultAdmin.address, + ); + await accessControl['grantRole(bytes32,address)']( + roles.common.greenlisted, + testUser.address, + ); + + return { + accessControl, + pauseManager, + timelockManager, + timelock, + mTBILL, + owner, + tokensReceiver, + feeReceiver, + requestRedeemer, + vaultAdmin, + testUser, + clawbackReceiver, + councilMembers, + regularUsers, + roles, + }; +} diff --git a/test/temp-prefer-const.test.ts b/test/temp-prefer-const.test.ts deleted file mode 100644 index 442bd6ef..00000000 --- a/test/temp-prefer-const.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -describe('test', () => { - it('should work', () => { - const owner = 'test'; - const customFeed = 'feed'; - console.log(owner, customFeed); - }); -}); diff --git a/test/unit/Blacklistable.test.ts b/test/unit/Blacklistable.test.ts index 732f00b1..d7548614 100644 --- a/test/unit/Blacklistable.test.ts +++ b/test/unit/Blacklistable.test.ts @@ -1,8 +1,7 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; -import { BlacklistableTester__factory } from '../../typechain-types'; -import { acErrors, blackList, unBlackList } from '../common/ac.helpers'; +import { acErrors, blackList } from '../common/ac.helpers'; import { defaultDeploy } from '../common/fixtures'; describe('Blacklistable', function () { @@ -19,30 +18,6 @@ describe('Blacklistable', function () { ).eq(true); }); - it('onlyInitializing', async () => { - const { accessControl, owner } = await loadFixture(defaultDeploy); - - const blackListable = await new BlacklistableTester__factory( - owner, - ).deploy(); - - await expect( - blackListable.initializeWithoutInitializer(accessControl.address), - ).revertedWith('Initializable: contract is not initializing'); - }); - - it('onlyInitializing unchained', async () => { - const { accessControl, owner } = await loadFixture(defaultDeploy); - - const blackListable = await new BlacklistableTester__factory( - owner, - ).deploy(); - - await expect( - blackListable.initializeUnchainedWithoutInitializer(), - ).revertedWith('Initializable: contract is not initializing'); - }); - describe('modifier onlyNotBlacklisted', () => { it('should fail: call from blacklisted user', async () => { const { accessControl, blackListableTester, owner, regularAccounts } = @@ -56,7 +31,10 @@ describe('Blacklistable', function () { blackListableTester.onlyNotBlacklistedTester( regularAccounts[0].address, ), - ).revertedWith(acErrors.WMAC_HAS_ROLE); + ).revertedWithCustomError( + blackListableTester, + acErrors.WMAC_BLACKLISTED().customErrorName, + ); }); it('call from not blacklisted user', async () => { @@ -70,54 +48,4 @@ describe('Blacklistable', function () { ).not.reverted; }); }); - - describe('addToBlackList', () => { - it('should fail: call from user without BLACKLIST_OPERATOR_ROLE role', async () => { - const { accessControl, blackListableTester, owner, regularAccounts } = - await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - { - from: regularAccounts[0], - revertMessage: `AccessControl: account ${regularAccounts[0].address.toLowerCase()} is missing role ${await accessControl.BLACKLIST_OPERATOR_ROLE()}`, - }, - ); - }); - - it('call from user with BLACKLIST_OPERATOR_ROLE role', async () => { - const { accessControl, blackListableTester, owner, regularAccounts } = - await loadFixture(defaultDeploy); - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - }); - }); - - describe('removeFromBlackList', () => { - it('should fail: call from user without BLACKLIST_OPERATOR_ROLE role', async () => { - const { accessControl, blackListableTester, owner, regularAccounts } = - await loadFixture(defaultDeploy); - - await unBlackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - { - from: regularAccounts[0], - revertMessage: `AccessControl: account ${regularAccounts[0].address.toLowerCase()} is missing role ${await accessControl.BLACKLIST_OPERATOR_ROLE()}`, - }, - ); - }); - - it('call from user with BLACKLIST_OPERATOR_ROLE role', async () => { - const { accessControl, blackListableTester, owner, regularAccounts } = - await loadFixture(defaultDeploy); - await unBlackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - }); - }); }); diff --git a/test/unit/CompositeDataFeed.test.ts b/test/unit/CompositeDataFeed.test.ts index 49ecfd53..780f6c86 100644 --- a/test/unit/CompositeDataFeed.test.ts +++ b/test/unit/CompositeDataFeed.test.ts @@ -3,8 +3,12 @@ import { expect } from 'chai'; import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; -import { CompositeDataFeedTest__factory } from '../../typechain-types'; +import { + CompositeDataFeed__factory, + CompositeDataFeedTest__factory, +} from '../../typechain-types'; import { acErrors } from '../common/ac.helpers'; +import { validateImplementation } from '../common/common.helpers'; import { setRoundData } from '../common/data-feed.helpers'; import { defaultDeploy } from '../common/fixtures'; @@ -30,6 +34,7 @@ describe('CompositeDataFeed', function () { expect(await compositeDataFeed.maxExpectedAnswer()).eq( parseUnits('10000', 18), ); + await validateImplementation(CompositeDataFeed__factory); }); it('initialize', async () => { @@ -90,7 +95,10 @@ describe('CompositeDataFeed', function () { compositeDataFeed .connect(regularAccounts[0]) .changeNumeratorFeed(ethers.constants.AddressZero), - ).revertedWith(acErrors.WMAC_HASNT_ROLE); + ).revertedWithCustomError( + compositeDataFeed, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); }); it('should fail: pass zero address', async () => { @@ -122,7 +130,10 @@ describe('CompositeDataFeed', function () { compositeDataFeed .connect(regularAccounts[0]) .changeDenominatorFeed(ethers.constants.AddressZero), - ).revertedWith(acErrors.WMAC_HASNT_ROLE); + ).revertedWithCustomError( + compositeDataFeed, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); }); it('should fail: pass zero address', async () => { @@ -154,7 +165,10 @@ describe('CompositeDataFeed', function () { compositeDataFeed .connect(regularAccounts[0]) .setMinExpectedAnswer(parseUnits('1')), - ).revertedWith(acErrors.WMAC_HASNT_ROLE); + ).revertedWithCustomError( + compositeDataFeed, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); }); it('should fail: pass value more than max expected answer', async () => { @@ -195,7 +209,10 @@ describe('CompositeDataFeed', function () { compositeDataFeed .connect(regularAccounts[0]) .setMaxExpectedAnswer(parseUnits('1')), - ).revertedWith(acErrors.WMAC_HASNT_ROLE); + ).revertedWithCustomError( + compositeDataFeed, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); }); it('should fail: pass value less than min expected answer', async () => { @@ -311,7 +328,7 @@ describe('CompositeDataFeed', function () { ); }); - it('should fail when: num. feed is unhealthy ', async () => { + it('should fail: when: num. feed is unhealthy ', async () => { const { compositeDataFeed, mockedAggregatorMToken } = await loadFixture( defaultDeploy, ); @@ -321,7 +338,7 @@ describe('CompositeDataFeed', function () { ); }); - it('should fail when: denom. feed is unhealthy', async () => { + it('should fail: when: denom. feed is unhealthy', async () => { const { compositeDataFeed, mockedAggregatorMBasis } = await loadFixture( defaultDeploy, ); diff --git a/test/unit/CustomFeed.test.ts b/test/unit/CustomFeed.test.ts index 47c2e4b2..9767159f 100644 --- a/test/unit/CustomFeed.test.ts +++ b/test/unit/CustomFeed.test.ts @@ -4,23 +4,33 @@ import { expect } from 'chai'; import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; +import { encodeFnSelector } from '../../helpers/utils'; import { CustomAggregatorV3CompatibleFeed__factory, CustomAggregatorV3CompatibleFeedTester__factory, - MBasisCustomAggregatorFeed__factory, - MTBillCustomAggregatorFeed__factory, } from '../../typechain-types'; -import { acErrors } from '../common/ac.helpers'; +import { + acErrors, + setPermissionRoleTester, + setRoleTimelocksTester, + setupGrantOperatorRole, +} from '../common/ac.helpers'; +import { keccak256, validateImplementation } from '../common/common.helpers'; import { calculatePriceDiviation, + setMaxAnswerDeviationTest, setRoundData, setRoundDataSafe, } from '../common/custom-feed.helpers'; import { defaultDeploy } from '../common/fixtures'; +import { + executeTimelockOperationTester, + bulkScheduleTimelockOperationTester, +} from '../common/timelock-manager.helpers'; describe('CustomAggregatorV3CompatibleFeed', function () { it('deployment', async () => { - const { customFeed, owner } = await loadFixture(defaultDeploy); + const { customFeed, owner, roles } = await loadFixture(defaultDeploy); expect(await customFeed.maxAnswer()).eq(parseUnits('10000', 8)); expect(await customFeed.minAnswer()).eq(2); @@ -31,17 +41,11 @@ describe('CustomAggregatorV3CompatibleFeed', function () { expect(await customFeed.latestRound()).eq(0); expect(await customFeed.lastAnswer()).eq(0); expect(await customFeed.lastTimestamp()).eq(0); - expect(await customFeed.feedAdminRole()).eq( - await customFeed.CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE(), + expect(await customFeed.contractAdminRole()).eq( + keccak256('CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE'), ); - const newFeed = await new CustomAggregatorV3CompatibleFeed__factory( - owner, - ).deploy(); - - expect(await newFeed.feedAdminRole()).eq( - await newFeed.DEFAULT_ADMIN_ROLE(), - ); + await validateImplementation(CustomAggregatorV3CompatibleFeed__factory); }); it('initialize', async () => { @@ -70,30 +74,6 @@ describe('CustomAggregatorV3CompatibleFeed', function () { ).revertedWith('CA: !max deviation'); }); - it('MBasisCustomAggregatorFeed', async () => { - const fixture = await loadFixture(defaultDeploy); - - const tester = await new MBasisCustomAggregatorFeed__factory( - fixture.owner, - ).deploy(); - - expect(await tester.feedAdminRole()).eq( - await tester.M_BASIS_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE(), - ); - }); - - it('MTBillCustomAggregatorFeed', async () => { - const fixture = await loadFixture(defaultDeploy); - - const tester = await new MTBillCustomAggregatorFeed__factory( - fixture.owner, - ).deploy(); - - expect(await tester.feedAdminRole()).eq( - await tester.M_TBILL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE(), - ); - }); - describe('setRoundData', async () => { it('call from owner', async () => { const fixture = await loadFixture(defaultDeploy); @@ -103,7 +83,7 @@ describe('CustomAggregatorV3CompatibleFeed', function () { const fixture = await loadFixture(defaultDeploy); await setRoundData(fixture, 10, { from: fixture.regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), }); }); @@ -137,7 +117,7 @@ describe('CustomAggregatorV3CompatibleFeed', function () { const fixture = await loadFixture(defaultDeploy); await setRoundDataSafe(fixture, 10, { from: fixture.regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), }); }); @@ -188,6 +168,252 @@ describe('CustomAggregatorV3CompatibleFeed', function () { }); }); + describe('setMaxAnswerDeviation()', () => { + const validMaxAnswerDeviation = parseUnits('0.5', 8); + const invalidMaxAnswerDeviation = parseUnits('101', 8); + const setMaxAnswerDeviationSelector = encodeFnSelector( + 'setMaxAnswerDeviation(uint256)', + ); + + it('call from owner', async () => { + const fixture = await loadFixture(defaultDeploy); + + await setMaxAnswerDeviationTest(fixture, validMaxAnswerDeviation); + }); + + it('should fail: call from non owner', async () => { + const fixture = await loadFixture(defaultDeploy); + + await setMaxAnswerDeviationTest(fixture, validMaxAnswerDeviation, { + from: fixture.regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }); + }); + + it('should fail: when maxAnswerDeviation is greater than 100%', async () => { + const fixture = await loadFixture(defaultDeploy); + + await setMaxAnswerDeviationTest(fixture, invalidMaxAnswerDeviation, { + revertMessage: 'CA: !max deviation', + }); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, customFeed, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + const user = regularAccounts[0]; + const feedAdminRole = await customFeed.contractAdminRole(); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: feedAdminRole, + targetContract: customFeed.address, + functionSelector: setMaxAnswerDeviationSelector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + customFeed.address, + setMaxAnswerDeviationSelector, + [{ account: user.address, enabled: true }], + ); + + expect(await accessControl.hasRole(feedAdminRole, user.address)).eq( + false, + ); + + await setMaxAnswerDeviationTest( + { customFeed, owner }, + validMaxAnswerDeviation, + { from: user }, + ); + }); + + it('succeeds with scoped permission and feed admin role', async () => { + const { accessControl, customFeed, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + const user = regularAccounts[0]; + const feedAdminRole = await customFeed.contractAdminRole(); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: feedAdminRole, + targetContract: customFeed.address, + functionSelector: setMaxAnswerDeviationSelector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + customFeed.address, + setMaxAnswerDeviationSelector, + [{ account: user.address, enabled: true }], + ); + + await accessControl['grantRole(bytes32,address)']( + feedAdminRole, + user.address, + ); + + await setMaxAnswerDeviationTest( + { customFeed, owner }, + validMaxAnswerDeviation, + { from: user }, + ); + }); + + it('when called through timelock with contract admin role', async () => { + const { + accessControl, + customFeed, + owner, + regularAccounts, + timelock, + timelockManager, + } = await loadFixture(defaultDeploy); + + const proposer = regularAccounts[0]; + const feedAdminRole = await customFeed.contractAdminRole(); + + await accessControl['grantRole(bytes32,address)']( + feedAdminRole, + proposer.address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [feedAdminRole], + [3600], + ); + + const calldata = customFeed.interface.encodeFunctionData( + 'setMaxAnswerDeviation', + [validMaxAnswerDeviation], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [customFeed.address], + [calldata], + {}, + { from: proposer }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + customFeed.address, + calldata, + proposer.address, + { from: owner }, + ); + + expect(await customFeed.maxAnswerDeviation()).eq(validMaxAnswerDeviation); + }); + + it('when called through timelock with function admin role', async () => { + const { + accessControl, + customFeed, + owner, + regularAccounts, + timelock, + timelockManager, + } = await loadFixture(defaultDeploy); + + const proposer = regularAccounts[0]; + const feedAdminRole = await customFeed.contractAdminRole(); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: feedAdminRole, + targetContract: customFeed.address, + functionSelector: setMaxAnswerDeviationSelector, + grantOperator: owner, + }); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: feedAdminRole, + targetContract: timelockManager.address, + functionSelector: setMaxAnswerDeviationSelector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + feedAdminRole, + customFeed.address, + setMaxAnswerDeviationSelector, + [{ account: proposer.address, enabled: true }], + ); + + await setPermissionRoleTester( + { accessControl, owner }, + feedAdminRole, + timelockManager.address, + setMaxAnswerDeviationSelector, + [{ account: proposer.address, enabled: true }], + ); + + expect(await accessControl.hasRole(feedAdminRole, proposer.address)).eq( + false, + ); + + const feedPermissionKey = await accessControl.permissionRoleKey( + feedAdminRole, + customFeed.address, + setMaxAnswerDeviationSelector, + ); + const timelockPermissionKey = await accessControl.permissionRoleKey( + feedAdminRole, + timelockManager.address, + setMaxAnswerDeviationSelector, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [feedPermissionKey, timelockPermissionKey], + [3600, 3600], + ); + + const calldata = customFeed.interface.encodeFunctionData( + 'setMaxAnswerDeviation', + [validMaxAnswerDeviation], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [customFeed.address], + [calldata], + {}, + { from: proposer }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + customFeed.address, + calldata, + proposer.address, + { from: owner }, + ); + + expect(await customFeed.maxAnswerDeviation()).eq(validMaxAnswerDeviation); + }); + }); + describe('_getDeviation', async () => { it('when new price is 0', async () => { const fixture = await loadFixture(defaultDeploy); diff --git a/test/unit/CustomFeedGrowth.test.ts b/test/unit/CustomFeedGrowth.test.ts index ebf70b35..00f6a6eb 100644 --- a/test/unit/CustomFeedGrowth.test.ts +++ b/test/unit/CustomFeedGrowth.test.ts @@ -4,13 +4,21 @@ import { expect } from 'chai'; import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; +import { encodeFnSelector } from '../../helpers/utils'; import { CustomAggregatorV3CompatibleFeedGrowth__factory, CustomAggregatorV3CompatibleFeedGrowthTester__factory, } from '../../typechain-types'; -import { acErrors } from '../common/ac.helpers'; +import { + acErrors, + setPermissionRoleTester, + setRoleTimelocksTester, + setupGrantOperatorRole, +} from '../common/ac.helpers'; +import { keccak256, validateImplementation } from '../common/common.helpers'; import { setMaxGrowthApr, + setMaxAnswerDeviationTest, setMinGrowthApr, setOnlyUp, setRoundDataGrowth, @@ -18,6 +26,10 @@ import { } from '../common/custom-feed-growth.helpers'; import { calculatePriceDiviation } from '../common/custom-feed.helpers'; import { defaultDeploy } from '../common/fixtures'; +import { + executeTimelockOperationTester, + bulkScheduleTimelockOperationTester, +} from '../common/timelock-manager.helpers'; describe('CustomAggregatorV3CompatibleFeedGrowth', function () { it('deployment', async () => { @@ -34,16 +46,12 @@ describe('CustomAggregatorV3CompatibleFeedGrowth', function () { expect(await customFeedGrowth.latestRound()).eq(0); expect(await customFeedGrowth.lastAnswer()).eq(0); expect(await customFeedGrowth.lastTimestamp()).eq(0); - expect(await customFeedGrowth.feedAdminRole()).eq( - await customFeedGrowth.CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE(), + expect(await customFeedGrowth.contractAdminRole()).eq( + keccak256('CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE'), ); - const newFeed = await new CustomAggregatorV3CompatibleFeedGrowth__factory( - owner, - ).deploy(); - - expect(await newFeed.feedAdminRole()).eq( - await newFeed.DEFAULT_ADMIN_ROLE(), + await validateImplementation( + CustomAggregatorV3CompatibleFeedGrowth__factory, ); }); @@ -183,7 +191,7 @@ describe('CustomAggregatorV3CompatibleFeedGrowth', function () { const fixture = await loadFixture(defaultDeploy); await setRoundDataGrowth(fixture, 10, -100, 0, { from: fixture.regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), }); }); @@ -342,13 +350,11 @@ describe('CustomAggregatorV3CompatibleFeedGrowth', function () { await setMinGrowthApr(fixture, -10); await setOnlyUp(fixture, true); - await fixture.customFeedGrowth.setMaxAnswerDeviation( - parseUnits('1000', 8), - ); + await setMaxAnswerDeviationTest(fixture, parseUnits('100', 8)); await setRoundDataSafeGrowth({ ...fixture }, 10, -3600, 0); - await setRoundDataSafeGrowth({ ...fixture }, 100, -1600, -1, { + await setRoundDataSafeGrowth({ ...fixture }, 11, -1600, -1, { revertMessage: 'CAG: negative apr', }); }); @@ -357,7 +363,7 @@ describe('CustomAggregatorV3CompatibleFeedGrowth', function () { const fixture = await loadFixture(defaultDeploy); await setRoundDataSafeGrowth(fixture, 10, -100, 0, { from: fixture.regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), }); }); @@ -465,7 +471,7 @@ describe('CustomAggregatorV3CompatibleFeedGrowth', function () { const fixture = await loadFixture(defaultDeploy); await setMinGrowthApr(fixture, 10, { from: fixture.regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), }); }); @@ -498,7 +504,7 @@ describe('CustomAggregatorV3CompatibleFeedGrowth', function () { const fixture = await loadFixture(defaultDeploy); await setMaxGrowthApr(fixture, 10, { from: fixture.regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), }); }); @@ -542,8 +548,236 @@ describe('CustomAggregatorV3CompatibleFeedGrowth', function () { const fixture = await loadFixture(defaultDeploy); await setOnlyUp(fixture, true, { from: fixture.regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }); + }); + }); + + describe('setMaxAnswerDeviation()', () => { + const validMaxAnswerDeviation = parseUnits('0.5', 8); + const invalidMaxAnswerDeviation = parseUnits('101', 8); + const setMaxAnswerDeviationSelector = encodeFnSelector( + 'setMaxAnswerDeviation(uint256)', + ); + + it('call from owner', async () => { + const fixture = await loadFixture(defaultDeploy); + + await setMaxAnswerDeviationTest(fixture, validMaxAnswerDeviation); + }); + + it('should fail: call from non owner', async () => { + const fixture = await loadFixture(defaultDeploy); + + await setMaxAnswerDeviationTest(fixture, validMaxAnswerDeviation, { + from: fixture.regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }); + }); + + it('should fail: when maxAnswerDeviation is greater than 100%', async () => { + const fixture = await loadFixture(defaultDeploy); + + await setMaxAnswerDeviationTest(fixture, invalidMaxAnswerDeviation, { + revertMessage: 'CAG: !max deviation', + }); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, customFeedGrowth, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + const user = regularAccounts[0]; + const feedAdminRole = await customFeedGrowth.contractAdminRole(); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: feedAdminRole, + targetContract: customFeedGrowth.address, + functionSelector: setMaxAnswerDeviationSelector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + customFeedGrowth.address, + setMaxAnswerDeviationSelector, + [{ account: user.address, enabled: true }], + ); + + expect(await accessControl.hasRole(feedAdminRole, user.address)).eq( + false, + ); + + await setMaxAnswerDeviationTest( + { customFeedGrowth, owner }, + validMaxAnswerDeviation, + { from: user }, + ); + }); + + it('succeeds with scoped permission and feed admin role', async () => { + const { accessControl, customFeedGrowth, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + const user = regularAccounts[0]; + const feedAdminRole = await customFeedGrowth.contractAdminRole(); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: feedAdminRole, + targetContract: customFeedGrowth.address, + functionSelector: setMaxAnswerDeviationSelector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + customFeedGrowth.address, + setMaxAnswerDeviationSelector, + [{ account: user.address, enabled: true }], + ); + + await accessControl['grantRole(bytes32,address)']( + feedAdminRole, + user.address, + ); + + await setMaxAnswerDeviationTest( + { customFeedGrowth, owner }, + validMaxAnswerDeviation, + { from: user }, + ); + }); + + it('when called through timelock with contract admin role', async () => { + const { + accessControl, + customFeedGrowth, + owner, + regularAccounts, + timelock, + timelockManager, + } = await loadFixture(defaultDeploy); + + const proposer = regularAccounts[0]; + const feedAdminRole = await customFeedGrowth.contractAdminRole(); + + await accessControl['grantRole(bytes32,address)']( + feedAdminRole, + proposer.address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [feedAdminRole], + [3600], + ); + + const calldata = customFeedGrowth.interface.encodeFunctionData( + 'setMaxAnswerDeviation', + [validMaxAnswerDeviation], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [customFeedGrowth.address], + [calldata], + {}, + { from: proposer }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + customFeedGrowth.address, + calldata, + proposer.address, + { from: owner }, + ); + + expect(await customFeedGrowth.maxAnswerDeviation()).eq( + validMaxAnswerDeviation, + ); + }); + + it('when called through timelock with function admin role', async () => { + const { + accessControl, + customFeedGrowth, + owner, + regularAccounts, + timelock, + timelockManager, + } = await loadFixture(defaultDeploy); + + const proposer = regularAccounts[0]; + const feedAdminRole = await customFeedGrowth.contractAdminRole(); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: feedAdminRole, + targetContract: customFeedGrowth.address, + functionSelector: setMaxAnswerDeviationSelector, + grantOperator: owner, }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + customFeedGrowth.address, + setMaxAnswerDeviationSelector, + [{ account: proposer.address, enabled: true }], + ); + + expect(await accessControl.hasRole(feedAdminRole, proposer.address)).eq( + false, + ); + + const feedPermissionKey = await accessControl.permissionRoleKey( + feedAdminRole, + customFeedGrowth.address, + setMaxAnswerDeviationSelector, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [feedPermissionKey], + [3600], + ); + + const calldata = customFeedGrowth.interface.encodeFunctionData( + 'setMaxAnswerDeviation', + [validMaxAnswerDeviation], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [customFeedGrowth.address], + [calldata], + {}, + { from: proposer }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + customFeedGrowth.address, + calldata, + proposer.address, + { from: owner }, + ); + + expect(await customFeedGrowth.maxAnswerDeviation()).eq( + validMaxAnswerDeviation, + ); }); }); diff --git a/test/unit/DataFeed.test.ts b/test/unit/DataFeed.test.ts index 4516947d..de705ba2 100644 --- a/test/unit/DataFeed.test.ts +++ b/test/unit/DataFeed.test.ts @@ -4,14 +4,28 @@ import { expect } from 'chai'; import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; -import { DataFeedTest__factory } from '../../typechain-types'; -import { acErrors } from '../common/ac.helpers'; +import { encodeFnSelector } from '../../helpers/utils'; +import { + DataFeed__factory, + DataFeedTest__factory, +} from '../../typechain-types'; +import { + acErrors, + setPermissionRoleTester, + setRoleTimelocksTester, + setupGrantOperatorRole, +} from '../common/ac.helpers'; +import { validateImplementation } from '../common/common.helpers'; import { setMinGrowthApr, setRoundDataGrowth, } from '../common/custom-feed-growth.helpers'; -import { setRoundData } from '../common/data-feed.helpers'; +import { setHealthyDiffTest, setRoundData } from '../common/data-feed.helpers'; import { defaultDeploy } from '../common/fixtures'; +import { + bulkScheduleTimelockOperationTester, + executeTimelockOperationTester, +} from '../common/timelock-manager.helpers'; describe('DataFeed', function () { it('deployment', async () => { @@ -26,6 +40,7 @@ describe('DataFeed', function () { expect(await dataFeed.maxExpectedAnswer()).eq( parseUnits('10000', mockedAggregatorDecimals), ); + await validateImplementation(DataFeed__factory); }); it('initialize', async () => { @@ -78,7 +93,10 @@ describe('DataFeed', function () { dataFeed .connect(regularAccounts[0]) .changeAggregator(ethers.constants.AddressZero), - ).revertedWith(acErrors.WMAC_HASNT_ROLE); + ).revertedWithCustomError( + dataFeed, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); }); it('should fail: pass zero address', async () => { @@ -97,6 +115,244 @@ describe('DataFeed', function () { }); }); + describe('setHealthyDiff()', () => { + const validHealthyDiff = 2 * 24 * 3600; + const invalidHealthyDiff = 0; + const setHealthyDiffSelector = encodeFnSelector('setHealthyDiff(uint256)'); + + it('call from owner', async () => { + const fixture = await loadFixture(defaultDeploy); + + await setHealthyDiffTest(fixture, validHealthyDiff); + }); + + it('should fail: call from non owner', async () => { + const fixture = await loadFixture(defaultDeploy); + + await setHealthyDiffTest(fixture, validHealthyDiff, { + from: fixture.regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }); + }); + + it('should fail: when healthy diff is 0', async () => { + const fixture = await loadFixture(defaultDeploy); + + await setHealthyDiffTest(fixture, invalidHealthyDiff, { + revertMessage: 'DF: invalid diff', + }); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, dataFeed, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + const user = regularAccounts[0]; + const feedAdminRole = await dataFeed.contractAdminRole(); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: feedAdminRole, + targetContract: dataFeed.address, + functionSelector: setHealthyDiffSelector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + dataFeed.address, + setHealthyDiffSelector, + [{ account: user.address, enabled: true }], + ); + + expect(await accessControl.hasRole(feedAdminRole, user.address)).eq( + false, + ); + + await setHealthyDiffTest({ dataFeed, owner }, validHealthyDiff, { + from: user, + }); + }); + + it('succeeds with scoped permission and feed admin role', async () => { + const { accessControl, dataFeed, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + const user = regularAccounts[0]; + const feedAdminRole = await dataFeed.contractAdminRole(); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: feedAdminRole, + targetContract: dataFeed.address, + functionSelector: setHealthyDiffSelector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + dataFeed.address, + setHealthyDiffSelector, + [{ account: user.address, enabled: true }], + ); + + await accessControl['grantRole(bytes32,address)']( + feedAdminRole, + user.address, + ); + + await setHealthyDiffTest({ dataFeed, owner }, validHealthyDiff, { + from: user, + }); + }); + + it('when called through timelock with contract admin role', async () => { + const { + accessControl, + dataFeed, + owner, + regularAccounts, + timelock, + timelockManager, + } = await loadFixture(defaultDeploy); + + const proposer = regularAccounts[0]; + const feedAdminRole = await dataFeed.contractAdminRole(); + + await accessControl['grantRole(bytes32,address)']( + feedAdminRole, + proposer.address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [feedAdminRole], + [3600], + ); + + const calldata = dataFeed.interface.encodeFunctionData('setHealthyDiff', [ + validHealthyDiff, + ]); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [dataFeed.address], + [calldata], + {}, + { from: proposer }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + dataFeed.address, + calldata, + proposer.address, + { from: owner }, + ); + + expect(await dataFeed.healthyDiff()).eq(validHealthyDiff); + }); + + it('when called through timelock with function admin role', async () => { + const { + accessControl, + dataFeed, + owner, + regularAccounts, + timelock, + timelockManager, + } = await loadFixture(defaultDeploy); + + const proposer = regularAccounts[0]; + const feedAdminRole = await dataFeed.contractAdminRole(); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: feedAdminRole, + targetContract: dataFeed.address, + functionSelector: setHealthyDiffSelector, + grantOperator: owner, + }); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: feedAdminRole, + targetContract: timelockManager.address, + functionSelector: setHealthyDiffSelector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + feedAdminRole, + dataFeed.address, + setHealthyDiffSelector, + [{ account: proposer.address, enabled: true }], + ); + + await setPermissionRoleTester( + { accessControl, owner }, + feedAdminRole, + timelockManager.address, + setHealthyDiffSelector, + [{ account: proposer.address, enabled: true }], + ); + + expect(await accessControl.hasRole(feedAdminRole, proposer.address)).eq( + false, + ); + + const feedPermissionKey = await accessControl.permissionRoleKey( + feedAdminRole, + dataFeed.address, + setHealthyDiffSelector, + ); + const timelockPermissionKey = await accessControl.permissionRoleKey( + feedAdminRole, + timelockManager.address, + setHealthyDiffSelector, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [feedPermissionKey, timelockPermissionKey], + [3600, 3600], + ); + + const calldata = dataFeed.interface.encodeFunctionData('setHealthyDiff', [ + validHealthyDiff, + ]); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [dataFeed.address], + [calldata], + {}, + { from: proposer }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + dataFeed.address, + calldata, + proposer.address, + { from: owner }, + ); + + expect(await dataFeed.healthyDiff()).eq(validHealthyDiff); + }); + }); + describe('getDataInBase18()', () => { it('data in base18 conversion for 4$ price', async () => { const { dataFeed, mockedAggregator } = await loadFixture(defaultDeploy); @@ -146,14 +402,15 @@ describe('DataFeed', function () { }); describe('DataFeed Deprecated', function () { - it('should fail when: feed is deprecated', async () => { - const { dataFeedDeprecated } = await loadFixture(defaultDeploy); + it('should fail: when: feed is deprecated', async () => { + const { deployDeprecatedFeed } = await loadFixture(defaultDeploy); + const { dataFeedDeprecated } = await deployDeprecatedFeed(); await expect(dataFeedDeprecated.getDataInBase18()).to.be.reverted; }); }); describe('DataFeed Deprecated with growth', function () { - it('should fail when: feed is deprecated (price < 0)', async () => { + it('should fail: when: feed is deprecated (price < 0)', async () => { const { dataFeedGrowth, ...fixture } = await loadFixture(defaultDeploy); await setMinGrowthApr(fixture, -1000000); await setRoundDataGrowth(fixture, 0.001, -1000000, -1000000); @@ -164,11 +421,12 @@ describe('DataFeed Deprecated with growth', function () { }); describe('DataFeed Unhealthy', function () { - it('should fail when: feed is unhealthy (by time)', async () => { - const { dataFeedUnhealthy } = await loadFixture(defaultDeploy); + it('should fail: when: feed is unhealthy (by time)', async () => { + const { deployUnhealthyFeed } = await loadFixture(defaultDeploy); + const { dataFeedUnhealthy } = await deployUnhealthyFeed(); await expect(dataFeedUnhealthy.getDataInBase18()).to.be.reverted; }); - it('should fail when: feed is unhealthy (by min answer)', async () => { + it('should fail: when: feed is unhealthy (by min answer)', async () => { const { dataFeed, mockedAggregator } = await loadFixture(defaultDeploy); await setRoundData({ mockedAggregator }, 0.1); await expect(dataFeed.getDataInBase18()).to.be.not.reverted; @@ -176,7 +434,7 @@ describe('DataFeed Unhealthy', function () { await expect(dataFeed.getDataInBase18()).to.be.reverted; }); - it('should fail when: feed is unhealthy (by max answer)', async () => { + it('should fail: when: feed is unhealthy (by max answer)', async () => { const { dataFeed, mockedAggregator } = await loadFixture(defaultDeploy); await setRoundData({ mockedAggregator }, 10000); await expect(dataFeed.getDataInBase18()).to.be.not.reverted; @@ -186,7 +444,7 @@ describe('DataFeed Unhealthy', function () { }); describe('DataFeed Unhealthy with growth', function () { - it('should fail when: feed is unhealthy (by time)', async () => { + it('should fail: when: feed is unhealthy (by time)', async () => { const { dataFeedGrowth, ...fixture } = await loadFixture(defaultDeploy); await setRoundDataGrowth(fixture, 0.1, -10, 0); @@ -195,7 +453,7 @@ describe('DataFeed Unhealthy with growth', function () { 'DF: feed is unhealthy', ); }); - it('should fail when: feed is unhealthy (by min answer)', async () => { + it('should fail: when: feed is unhealthy (by min answer)', async () => { const { dataFeedGrowth, ...fixture } = await loadFixture(defaultDeploy); await setRoundDataGrowth(fixture, 0.1, -100, 0); await expect(dataFeedGrowth.getDataInBase18()).to.be.not.reverted; @@ -205,7 +463,7 @@ describe('DataFeed Unhealthy with growth', function () { ); }); - it('should fail when: feed is unhealthy (by max answer)', async () => { + it('should fail: when: feed is unhealthy (by max answer)', async () => { const { dataFeedGrowth, ...fixture } = await loadFixture(defaultDeploy); await dataFeedGrowth.setMinExpectedAnswer(parseUnits('10', 8)); diff --git a/test/unit/DepositVault.test.ts b/test/unit/DepositVault.test.ts index 53f390e8..1529cbc7 100644 --- a/test/unit/DepositVault.test.ts +++ b/test/unit/DepositVault.test.ts @@ -1,6281 +1,148 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; -import { expect } from 'chai'; -import { constants } from 'ethers'; -import { parseUnits } from 'ethers/lib/utils'; -import { ethers } from 'hardhat'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { encodeFnSelector } from '../../helpers/utils'; import { + baseInitParamsDv, + depositVaultSuits, +} from './suits/deposit-vault.suits'; + +import { + DepositVault__factory, DepositVaultTest__factory, - EUsdDepositVault__factory, - ManageableVaultTester__factory, - MBasisDepositVault__factory, } from '../../typechain-types'; -import { acErrors, blackList, greenList } from '../common/ac.helpers'; import { approveBase18, mintToken, - pauseVault, - pauseVaultFn, + validateImplementation, } from '../common/common.helpers'; -import { - setMinGrowthApr, - setRoundDataGrowth, -} from '../common/custom-feed-growth.helpers'; import { setRoundData } from '../common/data-feed.helpers'; -import { - approveRequestTest, - depositInstantTest, - depositRequestTest, - rejectRequestTest, - safeApproveRequestTest, - safeBulkApproveRequestTest, - setMaxSupplyCapTest, -} from '../common/deposit-vault.helpers'; +import { depositInstantTest } from '../common/deposit-vault.helpers'; import { defaultDeploy, mTokenPermissionedFixture } from '../common/fixtures'; -import { greenListEnable } from '../common/greenlist.helpers'; -import { - addPaymentTokenTest, - addWaivedFeeAccountTest, - changeTokenAllowanceTest, - removePaymentTokenTest, - removeWaivedFeeAccountTest, - setInstantFeeTest, - setInstantDailyLimitTest, - setMinAmountTest, - setMinAmountToDepositTest, - setVariabilityToleranceTest, - withdrawTest, - changeTokenFeeTest, -} from '../common/manageable-vault.helpers'; -import { sanctionUser } from '../common/with-sanctions-list.helpers'; - -describe('DepositVault', function () { - it('deployment', async () => { - const { - depositVault, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - roles, - } = await loadFixture(defaultDeploy); - - expect(await depositVault.mToken()).eq(mTBILL.address); - - expect(await depositVault.paused()).eq(false); - - expect(await depositVault.tokensReceiver()).eq(tokensReceiver.address); - expect(await depositVault.feeReceiver()).eq(feeReceiver.address); - - expect(await depositVault.ONE_HUNDRED_PERCENT()).eq('10000'); - - expect(await depositVault.minMTokenAmountForFirstDeposit()).eq('0'); - expect(await depositVault.minAmount()).eq(parseUnits('100')); - - expect(await depositVault.maxSupplyCap()).eq(constants.MaxUint256); - - expect(await depositVault.instantFee()).eq('100'); - - expect(await depositVault.instantDailyLimit()).eq(parseUnits('100000')); - - expect(await depositVault.mTokenDataFeed()).eq(mTokenToUsdDataFeed.address); - expect(await depositVault.variationTolerance()).eq(1); - - expect(await depositVault.vaultRole()).eq( - roles.tokenRoles.mTBILL.depositVaultAdmin, - ); - - expect(await depositVault.MANUAL_FULLFILMENT_TOKEN()).eq( - ethers.constants.AddressZero, - ); - }); - - it('failing deployment', async () => { - const { - accessControl, - mTBILL, - owner, - mTokenToUsdDataFeed, - feeReceiver, - tokensReceiver, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - const depositVault = await new DepositVaultTest__factory(owner).deploy(); - - await expect( - depositVault.initialize( - ethers.constants.AddressZero, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - parseUnits('100'), - constants.MaxUint256, - ), - ).to.be.reverted; - await expect( - depositVault.initialize( - accessControl.address, - { - mToken: ethers.constants.AddressZero, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - parseUnits('100'), - constants.MaxUint256, - ), - ).to.be.reverted; - await expect( - depositVault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: ethers.constants.AddressZero, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - parseUnits('100'), - constants.MaxUint256, - ), - ).to.be.reverted; - await expect( - depositVault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: ethers.constants.AddressZero, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - parseUnits('100'), - constants.MaxUint256, - ), - ).to.be.reverted; - await expect( - depositVault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: ethers.constants.AddressZero, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - parseUnits('100'), - constants.MaxUint256, - ), - ).to.be.reverted; - await expect( - depositVault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100001, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - parseUnits('100'), - constants.MaxUint256, - ), - ).to.be.reverted; - }); - - it('MBasisDepositVault', async () => { - const fixture = await loadFixture(defaultDeploy); - - const tester = await new MBasisDepositVault__factory( - fixture.owner, - ).deploy(); - - expect(await tester.vaultRole()).eq( - await tester.M_BASIS_DEPOSIT_VAULT_ADMIN_ROLE(), - ); - }); - - it('EUsdDepositVault', async () => { - const fixture = await loadFixture(defaultDeploy); - - const tester = await new EUsdDepositVault__factory(fixture.owner).deploy(); - - expect(await tester.vaultRole()).eq( - await tester.E_USD_DEPOSIT_VAULT_ADMIN_ROLE(), - ); - }); - - describe('initialization', () => { - it('should fail: cal; initialize() when already initialized', async () => { - const { depositVault } = await loadFixture(defaultDeploy); - - await expect( - depositVault.initialize( - constants.AddressZero, - { - mToken: constants.AddressZero, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: constants.AddressZero, - tokensReceiver: constants.AddressZero, - }, - { - instantFee: 0, - instantDailyLimit: 0, - }, - constants.AddressZero, - 0, - 0, - 0, - 0, - ), - ).revertedWith('Initializable: contract is already initialized'); - }); - - it('should fail: cal; initializeV1() when already initialized', async () => { - const { depositVault } = await loadFixture(defaultDeploy); - - await expect( - depositVault.initializeV1( - constants.AddressZero, - { - mToken: constants.AddressZero, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: constants.AddressZero, - tokensReceiver: constants.AddressZero, - }, - { - instantFee: 0, - instantDailyLimit: 0, - }, - constants.AddressZero, - 0, - 0, - 0, - ), - ).revertedWith('Initializable: contract is already initialized'); - }); - - it('should fail: cal; initializeV2() when already reinitialized', async () => { - const { depositVault } = await loadFixture(defaultDeploy); - - await expect( - depositVault.initializeV2(constants.MaxUint256), - ).revertedWith('Initializable: contract is already initialized'); - }); - - it('should fail: call with initializing == false', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initializeWithoutInitializer( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - ), - ).revertedWith('Initializable: contract is not initializing'); - }); - - it('should fail: when _tokensReceiver == address(this)', async () => { - const { - owner, - accessControl, - mTBILL, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: vault.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - ), - ).revertedWith('invalid address'); - }); - it('should fail: when _feeReceiver == address(this)', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: vault.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - ), - ).revertedWith('invalid address'); - }); - it('should fail: when limit = 0', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: 0, - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - ), - ).revertedWith('zero limit'); - }); - it('should fail: when mToken dataFeed address zero', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - ), - ).revertedWith('zero address'); - }); - it('should fail: when variationTolarance zero', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mockedSanctionsList, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 0, - parseUnits('100'), - ), - ).revertedWith('fee == 0'); - }); - }); - - describe('setMinMTokenAmountForFirstDeposit()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, depositVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - - await setMinAmountToDepositTest({ depositVault, owner }, 1.1, { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, depositVault } = await loadFixture(defaultDeploy); - await setMinAmountToDepositTest({ depositVault, owner }, 1.1); - }); - }); - - describe('setMaxSupplyCap()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, depositVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - - await setMaxSupplyCapTest({ depositVault, owner }, 1.1, { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, depositVault } = await loadFixture(defaultDeploy); - await setMaxSupplyCapTest({ depositVault, owner }, 1.1); - }); - }); - - describe('setMinAmount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, depositVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - - await setMinAmountTest({ vault: depositVault, owner }, 1.1, { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, depositVault } = await loadFixture(defaultDeploy); - await setMinAmountTest({ vault: depositVault, owner }, 1.1); - }); - }); - - describe('setInstantDailyLimit()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, depositVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - - await setInstantDailyLimitTest( - { vault: depositVault, owner }, - parseUnits('1000'), - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('should fail: try to set 0 limit', async () => { - const { owner, depositVault } = await loadFixture(defaultDeploy); - - await setInstantDailyLimitTest( - { vault: depositVault, owner }, - constants.Zero, - { - revertMessage: 'MV: limit zero', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, depositVault } = await loadFixture(defaultDeploy); - await setInstantDailyLimitTest( - { vault: depositVault, owner }, - parseUnits('1000'), - ); - }); - }); - - describe('addPaymentToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - ethers.constants.AddressZero, - ethers.constants.AddressZero, - 0, - false, - constants.MaxUint256, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when token is already added', async () => { - const { depositVault, stableCoins, owner, dataFeed } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - constants.MaxUint256, - { - revertMessage: 'MV: already added', - }, - ); - }); - - it('should fail: when token dataFeed address zero', async () => { - const { depositVault, stableCoins, owner } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - constants.AddressZero, - 0, - false, - constants.MaxUint256, - { - revertMessage: 'zero address', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, stableCoins, owner, dataFeed } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - ); - }); - - it('call when allowance is zero', async () => { - const { depositVault, stableCoins, owner, dataFeed } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - constants.Zero, - ); - }); - - it('call when allowance is not uint256 max', async () => { - const { depositVault, stableCoins, owner, dataFeed } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - parseUnits('100'), - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role and add 3 options on a row', async () => { - const { depositVault, stableCoins, owner, dataFeed } = await loadFixture( - defaultDeploy, - ); - - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - }); - }); - - describe('addWaivedFeeAccount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await addWaivedFeeAccountTest( - { vault: depositVault, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if account fee already waived', async () => { - const { depositVault, owner } = await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVault, owner }, - owner.address, - ); - await addWaivedFeeAccountTest( - { vault: depositVault, owner }, - owner.address, - { revertMessage: 'MV: already added' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, owner } = await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVault, owner }, - owner.address, - ); - }); - }); - - describe('removeWaivedFeeAccount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await removeWaivedFeeAccountTest( - { vault: depositVault, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if account not found in restriction', async () => { - const { depositVault, owner } = await loadFixture(defaultDeploy); - await removeWaivedFeeAccountTest( - { vault: depositVault, owner }, - owner.address, - { revertMessage: 'MV: not found' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, owner } = await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVault, owner }, - owner.address, - ); - await removeWaivedFeeAccountTest( - { vault: depositVault, owner }, - owner.address, - ); - }); - }); - - describe('setFee()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await setInstantFeeTest( - { vault: depositVault, owner }, - ethers.constants.Zero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: if new value greater then 100%', async () => { - const { depositVault, owner } = await loadFixture(defaultDeploy); - await setInstantFeeTest({ vault: depositVault, owner }, 10001, { - revertMessage: 'fee > 100%', +import { addPaymentTokenTest } from '../common/manageable-vault.helpers'; +import { initializeDv } from '../common/vault-initializer.helpers'; + +depositVaultSuits( + 'DepositVault', + defaultDeploy, + { + createNew: async (owner: SignerWithAddress) => + new DepositVaultTest__factory(owner).deploy(), + key: 'depositVault', + }, + async () => { + await validateImplementation(DepositVault__factory); + }, + { + deployUninitialized: (fixture) => + new DepositVaultTest__factory(fixture.owner).deploy(), + initialize: async (fixture, params, opt) => { + await initializeDv( + { ...baseInitParamsDv(fixture), ...params }, + opt?.contract, + opt, + ); + }, + }, + (defaultDeploy) => { + describe('DepositVault', function () { + describe('depositInstant() with permissioned mToken', () => { + it('with permissioned mToken - deposit instant mints mToken to greenlisted user', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mTokenPermissioned, + mTokenPermissionedRoles, + mTokenPermissionedDepositVault, + } = await loadFixture( + mTokenPermissionedFixture.bind(this, baseFixture), + ); + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + owner.address, + ); + await addPaymentTokenTest( + { vault: mTokenPermissionedDepositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await mintToken(stableCoins.dai, owner, 100_000); + await approveBase18( + owner, + stableCoins.dai, + mTokenPermissionedDepositVault, + 100_000, + ); + + await depositInstantTest( + { + depositVault: mTokenPermissionedDepositVault, + owner, + mTBILL: mTokenPermissioned, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 1000, + ); + }); + + it('should fail: with permissioned mToken - deposit instant mints mToken to non-greenlisted user', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mTokenPermissioned, + mTokenPermissionedDepositVault, + } = await loadFixture( + mTokenPermissionedFixture.bind(this, baseFixture), + ); + + await addPaymentTokenTest( + { vault: mTokenPermissionedDepositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await mintToken(stableCoins.dai, owner, 100_000); + await approveBase18( + owner, + stableCoins.dai, + mTokenPermissionedDepositVault, + 100_000, + ); + + await depositInstantTest( + { + depositVault: mTokenPermissionedDepositVault, + owner, + mTBILL: mTokenPermissioned, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 1000, + { + revertCustomError: { + customErrorName: 'NotGreenlisted', + }, + }, + ); + }); }); }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, owner } = await loadFixture(defaultDeploy); - await setInstantFeeTest({ vault: depositVault, owner }, 100); - }); - }); - - describe('setVariabilityTolerance()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await setVariabilityToleranceTest( - { vault: depositVault, owner }, - ethers.constants.Zero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if new value zero', async () => { - const { depositVault, owner } = await loadFixture(defaultDeploy); - await setVariabilityToleranceTest( - { vault: depositVault, owner }, - ethers.constants.Zero, - { revertMessage: 'fee == 0' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, owner } = await loadFixture(defaultDeploy); - await setVariabilityToleranceTest({ vault: depositVault, owner }, 100); - }); - }); - - describe('removePaymentToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await removePaymentTokenTest( - { vault: depositVault, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when token is not exists', async () => { - const { owner, depositVault, stableCoins } = await loadFixture( - defaultDeploy, - ); - await removePaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai.address, - { revertMessage: 'MV: not exists' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, stableCoins, owner, dataFeed } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await removePaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai.address, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role and add 3 options on a row', async () => { - const { depositVault, owner, stableCoins, dataFeed } = await loadFixture( - defaultDeploy, - ); - - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - - await removePaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai.address, - ); - await removePaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdc.address, - ); - await removePaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdt.address, - ); - - await removePaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdt.address, - { revertMessage: 'MV: not exists' }, - ); - }); - }); - - describe('withdrawToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await withdrawTest( - { vault: depositVault, owner }, - ethers.constants.AddressZero, - 0, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when there is no token in vault', async () => { - const { owner, depositVault, regularAccounts, stableCoins } = - await loadFixture(defaultDeploy); - await withdrawTest( - { vault: depositVault, owner }, - stableCoins.dai, - 1, - regularAccounts[0], - { revertMessage: 'ERC20: transfer amount exceeds balance' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, regularAccounts, stableCoins, owner } = - await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, depositVault, 1); - await withdrawTest( - { vault: depositVault, owner }, - stableCoins.dai, - 1, - regularAccounts[0], - ); - }); - }); - - describe('freeFromMinAmount()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { depositVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVault - .connect(regularAccounts[0]) - .freeFromMinAmount(regularAccounts[1].address, true), - ).to.be.revertedWith('WMAC: hasnt role'); - }); - it('should not fail', async () => { - const { depositVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVault.freeFromMinAmount(regularAccounts[0].address, true), - ).to.not.reverted; - - expect( - await depositVault.isFreeFromMinAmount(regularAccounts[0].address), - ).to.eq(true); - }); - it('should fail: already in list', async () => { - const { depositVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVault.freeFromMinAmount(regularAccounts[0].address, true), - ).to.not.reverted; - - expect( - await depositVault.isFreeFromMinAmount(regularAccounts[0].address), - ).to.eq(true); - - await expect( - depositVault.freeFromMinAmount(regularAccounts[0].address, true), - ).to.revertedWith('DV: already free'); - }); - }); - - describe('changeTokenAllowance()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await changeTokenAllowanceTest( - { vault: depositVault, owner }, - ethers.constants.AddressZero, - 0, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: token not exist', async () => { - const { depositVault, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - await changeTokenAllowanceTest( - { vault: depositVault, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: token not exists' }, - ); - }); - it('should fail: allowance zero', async () => { - const { depositVault, owner, stableCoins, dataFeed } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: depositVault, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: zero allowance' }, - ); - }); - it('should fail: if mint exceed allowance', async () => { - const { - depositVault, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100000); - await approveBase18(owner, stableCoins.dai, depositVault, 100000); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: depositVault, owner }, - stableCoins.dai.address, - 100, - ); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, owner, stableCoins, dataFeed } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: depositVault, owner }, - stableCoins.dai.address, - 100000000, - ); - }); - it('should decrease if allowance < UINT_MAX', async () => { - const { - depositVault, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100000); - await approveBase18(owner, stableCoins.dai, depositVault, 100000); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: depositVault, owner }, - stableCoins.dai.address, - parseUnits('1000'), - ); - - const tokenConfigBefore = await depositVault.tokensConfig( - stableCoins.dai.address, - ); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 999, - ); - - const tokenConfigAfter = await depositVault.tokensConfig( - stableCoins.dai.address, - ); - - expect(tokenConfigBefore.allowance.sub(tokenConfigAfter.allowance)).eq( - parseUnits('999'), - ); - }); - it('should not decrease if allowance = UINT_MAX', async () => { - const { - depositVault, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100000); - await approveBase18(owner, stableCoins.dai, depositVault, 100000); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: depositVault, owner }, - stableCoins.dai.address, - constants.MaxUint256, - ); - - const tokenConfigBefore = await depositVault.tokensConfig( - stableCoins.dai.address, - ); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 999, - ); - - const tokenConfigAfter = await depositVault.tokensConfig( - stableCoins.dai.address, - ); - - expect(tokenConfigBefore.allowance).eq(tokenConfigAfter.allowance); - }); - }); - - describe('changeTokenFee()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await changeTokenFeeTest( - { vault: depositVault, owner }, - ethers.constants.AddressZero, - 0, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: token not exist', async () => { - const { depositVault, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - await changeTokenFeeTest( - { vault: depositVault, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: token not exists' }, - ); - }); - it('should fail: fee > 100%', async () => { - const { depositVault, owner, stableCoins, dataFeed } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: depositVault, owner }, - stableCoins.dai.address, - 10001, - { revertMessage: 'fee > 100%' }, - ); - }); - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { depositVault, owner, stableCoins, dataFeed } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: depositVault, owner }, - stableCoins.dai.address, - 100, - ); - }); - }); - - describe('depositInstant()', async () => { - it('should fail: when there is no token in vault', async () => { - const { owner, depositVault, stableCoins, mTBILL, mTokenToUsdDataFeed } = - await loadFixture(defaultDeploy); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to deposit 0 amount', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 0, - { - revertMessage: 'DV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'depositInstant(address,uint256,uint256,bytes32)', - ); - await pauseVaultFn(depositVault, selector); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: when rounding is invalid', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100.0000000001, - { - revertMessage: 'MV: invalid rounding', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, depositVault, 10); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(stableCoins.dai, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minAmountToDepositTest', async () => { - const { - depositVault, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 100_000); - await approveBase18(owner, stableCoins.dai, depositVault, 100_000); - - await setMinAmountToDepositTest({ depositVault, owner }, 100_000); - await setInstantDailyLimitTest({ vault: depositVault, owner }, 150_000); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 99_999, - { - revertMessage: 'DV: mint amount < min', - }, - ); - }); - - it('should fail: call for amount < minAmount', async () => { - const { - depositVault, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 100_000); - await approveBase18(owner, stableCoins.dai, depositVault, 100_000); - - await setMinAmountToDepositTest({ depositVault, owner }, 100_000); - await setInstantDailyLimitTest({ vault: depositVault, owner }, 150_000); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 99, - { - revertMessage: 'DV: mToken amount < min', - }, - ); - }); - - it('should fail: if exceed allowance of deposit for token', async () => { - const { - depositVault, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.dai, owner, 100_000); - await changeTokenAllowanceTest( - { vault: depositVault, owner }, - stableCoins.dai.address, - 100, - ); - await approveBase18(owner, stableCoins.dai, depositVault, 100_000); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - - it('should fail: if mint limit exceeded', async () => { - const { - depositVault, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.dai, owner, 100_000); - await setInstantDailyLimitTest({ vault: depositVault, owner }, 1000); - - await approveBase18(owner, stableCoins.dai, depositVault, 100_000); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed limit', - }, - ); - }); - - it('should fail: if min receive amount greater then actual', async () => { - const { - depositVault, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.dai, owner, 100_000); - - await approveBase18(owner, stableCoins.dai, depositVault, 100_000); - - await depositInstantTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - minAmount: parseUnits('100000'), - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'DV: minReceiveAmount > actual', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 10000, - true, - ); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'DV: mToken amount < min', - }, - ); - - await removePaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: depositVault, owner }, 10000); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { revertMessage: 'DV: mToken amount < min' }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist ', async () => { - const { owner, depositVault, stableCoins, mTBILL, mTokenToUsdDataFeed } = - await loadFixture(defaultDeploy); - - await depositVault.setGreenlistEnable(true); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist ', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await depositVault.setGreenlistEnable(true); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: when function paused (custom recipient overload)', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'depositInstant(address,uint256,uint256,bytes32,address)', - ); - await pauseVaultFn(depositVault, selector); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: when 0 supply cap is left', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setMaxSupplyCapTest({ depositVault, owner }, 99); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setInstantFeeTest({ vault: depositVault, owner }, 0); - await setMinAmountTest({ vault: depositVault, owner }, 0); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 99, - { - from: regularAccounts[0], - }, - ); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'DV: max supply cap exceeded', - }, - ); - }); - - it('should fail: when 10 supply cap is left and try to mint 11', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 101); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 101, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setMaxSupplyCapTest({ depositVault, owner }, 100); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setInstantFeeTest({ vault: depositVault, owner }, 0); - await setMinAmountTest({ vault: depositVault, owner }, 0); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 90, - { - from: regularAccounts[0], - }, - ); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 11, - { - from: regularAccounts[0], - revertMessage: 'DV: max supply cap exceeded', - }, - ); - }); - - it('when 10 supply cap is left and try to mint 10', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setMaxSupplyCapTest({ depositVault, owner }, 100); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setInstantFeeTest({ vault: depositVault, owner }, 0); - await setMinAmountTest({ vault: depositVault, owner }, 0); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 90, - { - from: regularAccounts[0], - }, - ); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 10, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI, greenlist enabled and user in greenlist ', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await depositVault.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI when 10% growth is applied', async () => { - const { - owner, - depositVault, - customFeedGrowth, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await depositVault.setGreenlistEnable(true); - - await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); - await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, 10); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositInstantTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - expectedMintAmount: parseUnits('98.999684191007430686', 18), - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI when -10% growth is applied', async () => { - const { - owner, - depositVault, - customFeedGrowth, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await depositVault.setGreenlistEnable(true); - - await setMinGrowthApr({ owner, customFeedGrowth }, -10); - await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); - await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, -10); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositInstantTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - expectedMintAmount: parseUnits('99.000315811007437113', 18), - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI, greenlist enabled and user in greenlist, tokenIn not stablecoin', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await depositVault.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI, when price of stable is 1.03$ and mToken price is 5$', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - }); - - it('deposit 100 DAI, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - }); - - it('deposit 100 DAI, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await depositVault.freeFromMinAmount(owner.address, true); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - }); - - it('deposit 100 DAI, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await addWaivedFeeAccountTest( - { vault: depositVault, owner }, - owner.address, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, waivedFee: true }, - stableCoins.dai, - 100, - ); - }); - - it('deposit 100 DAI (custom recipient overload)', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositInstantTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI when recipient == msg.sender (custom recipient overload)', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositInstantTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[0], - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI when other overload of depositInstant is paused (custom recipient overload)', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - depositVault, - encodeFnSelector('depositInstant(address,uint256,uint256,bytes32)'), - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositInstantTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI when other overload of depositInstant is paused', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - depositVault, - encodeFnSelector( - 'depositInstant(address,uint256,uint256,bytes32,address)', - ), - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositInstantTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('with permissioned mToken - deposit instant mints mToken to greenlisted user', async () => { - const baseFixture = await defaultDeploy(); - const { - owner, - accessControl, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mTokenPermissioned, - mTokenPermissionedRoles, - mTokenPermissionedDepositVault, - } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); - - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - owner.address, - ); - await addPaymentTokenTest( - { vault: mTokenPermissionedDepositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 100_000); - await approveBase18( - owner, - stableCoins.dai, - mTokenPermissionedDepositVault, - 100_000, - ); - - await depositInstantTest( - { - depositVault: mTokenPermissionedDepositVault, - owner, - mTBILL: mTokenPermissioned, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1000, - ); - }); - - it('should fail: with permissioned mToken - deposit instant mints mToken to non-greenlisted user', async () => { - const baseFixture = await defaultDeploy(); - const { - owner, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mTokenPermissioned, - mTokenPermissionedDepositVault, - } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); - - await addPaymentTokenTest( - { vault: mTokenPermissionedDepositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 100_000); - await approveBase18( - owner, - stableCoins.dai, - mTokenPermissionedDepositVault, - 100_000, - ); - - await depositInstantTest( - { - depositVault: mTokenPermissionedDepositVault, - owner, - mTBILL: mTokenPermissioned, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1000, - { - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - }); - - describe('depositRequest()', async () => { - it('should fail: when there is no token in vault', async () => { - const { owner, depositVault, stableCoins, mTBILL, mTokenToUsdDataFeed } = - await loadFixture(defaultDeploy); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to deposit 0 amount', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 0, - { - revertMessage: 'DV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'depositRequest(address,uint256,bytes32)', - ); - await pauseVaultFn(depositVault, selector); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: when function paused (custom recipient overload)', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'depositRequest(address,uint256,bytes32,address)', - ); - await pauseVaultFn(depositVault, selector); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: when rounding is invalid', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100.0000000001, - { - revertMessage: 'MV: invalid rounding', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, depositVault, 10); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(stableCoins.dai, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minAmountToDepositTest', async () => { - const { - depositVault, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 100_000); - await approveBase18(owner, stableCoins.dai, depositVault, 100_000); - - await setMinAmountToDepositTest({ depositVault, owner }, 100_000); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 99_999, - { - revertMessage: 'DV: mint amount < min', - }, - ); - }); - - it('should fail: if exceed allowance of deposit for token', async () => { - const { - depositVault, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.dai, owner, 100_000); - await changeTokenAllowanceTest( - { vault: depositVault, owner }, - stableCoins.dai.address, - 100, - ); - await approveBase18(owner, stableCoins.dai, depositVault, 100_000); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - - it('should fail: if token fee = 100%', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 10000, - true, - ); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'DV: mToken amount < min', - }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist ', async () => { - const { owner, depositVault, stableCoins, mTBILL, mTokenToUsdDataFeed } = - await loadFixture(defaultDeploy); - - await depositVault.setGreenlistEnable(true); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist ', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctionlist ', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - } = await loadFixture(defaultDeploy); - - await depositVault.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctionlist (custom recipient overload)', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('deposit 100 DAI, greenlist enabled and user in greenlist ', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await greenListEnable( - { greenlistable: greenListableTester, owner }, - true, - ); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI when 10% growth is applied', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - customFeedGrowth, - } = await loadFixture(defaultDeploy); - - await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); - await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, 10); - - await greenListEnable( - { greenlistable: greenListableTester, owner }, - true, - ); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI when -10% growth is applied', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - customFeedGrowth, - } = await loadFixture(defaultDeploy); - - await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); - await setMinGrowthApr({ owner, customFeedGrowth }, -10); - await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, -10); - - await greenListEnable( - { greenlistable: greenListableTester, owner }, - true, - ); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit request with 100 DAI, when price of stable is 1.03$ and mToken price is 5$', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - }); - - it('deposit request with 100 DAI, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - }); - - it('deposit request with 100 DAI, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await depositVault.freeFromMinAmount(owner.address, true); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - }); - - it('deposit request with 100 DAI, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await addWaivedFeeAccountTest( - { vault: depositVault, owner }, - owner.address, - ); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, waivedFee: true }, - stableCoins.dai, - 100, - ); - }); - - it('deposit 100 (custom recipient overload)', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 when recipient == msg.sender (custom recipient overload)', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[0], - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI when other overload of depositRequest is paused (custom recipient overload)', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - depositVault, - encodeFnSelector('depositRequest(address,uint256,bytes32)'), - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI when other overload of depositRequest is paused', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - depositVault, - encodeFnSelector('depositRequest(address,uint256,bytes32,address)'), - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - }); - - describe('approveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { depositVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = - await loadFixture(defaultDeploy); - await approveRequestTest( - { - depositVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('5'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await approveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - parseUnits('5'), - { - revertMessage: 'DV: request not exist', - }, - ); - }); - - it('should fail: request already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - parseUnits('5'), - ); - await approveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - parseUnits('5'), - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - parseUnits('5'), - ); - }); - }); - - describe('safeApproveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { depositVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = - await loadFixture(defaultDeploy); - await safeApproveRequestTest( - { - depositVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeApproveRequestTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'DV: request not exist', - }, - ); - }); - - it('should fail: if new rate greater then variabilityTolerance', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - await safeApproveRequestTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('6'), - { - revertMessage: 'MV: exceed price diviation', - }, - ); - }); - - it('should fail: if new rate lower then variabilityTolerance', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - await safeApproveRequestTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('4'), - { - revertMessage: 'MV: exceed price diviation', - }, - ); - }); - - it('should fail: request already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - parseUnits('5.000001'), - ); - await safeApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - parseUnits('5.000001'), - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('should fail: when 0 supply cap is left', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setMaxSupplyCapTest({ depositVault, owner }, 99); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: depositVault, owner }, 0); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 99, - { - from: regularAccounts[0], - }, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - }, - ); - - await safeApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - parseUnits('1'), - { - revertMessage: 'DV: max supply cap exceeded', - }, - ); - }); - - it('should fail: when 10 supply cap is left and try to mint 11', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 101); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 101, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setMaxSupplyCapTest({ depositVault, owner }, 100); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: depositVault, owner }, 0); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 90, - { - from: regularAccounts[0], - }, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 11, - { - from: regularAccounts[0], - }, - ); - - await safeApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - parseUnits('1'), - { - revertMessage: 'DV: max supply cap exceeded', - }, - ); - }); - - it('when 10 supply cap is left and try to mint 10', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setMaxSupplyCapTest({ depositVault, owner }, 100); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setInstantFeeTest({ vault: depositVault, owner }, 0); - await setMinAmountTest({ vault: depositVault, owner }, 0); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 90, - { - from: regularAccounts[0], - }, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 10, - { - from: regularAccounts[0], - }, - ); - - await safeApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - parseUnits('1'), - ); - }); - - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - parseUnits('5.000000001'), - ); - }); - }); - - describe('safeBulkApproveRequestAtSavedRate()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { depositVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = - await loadFixture(defaultDeploy); - await safeBulkApproveRequestTest( - { - depositVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }], - 'request-rate', - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeBulkApproveRequestTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }], - 'request-rate', - { - revertMessage: 'DV: request not exist', - }, - ); - }); - - it('should fail: request already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - 'request-rate', - ); - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - 'request-rate', - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when one of them already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVault, 200); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - parseUnits('5.000001'), - ); - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0 }], - 'request-rate', - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when couple of them have equal id', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVault, 200); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - parseUnits('5.000001'), - ); - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 1 }], - 'request-rate', - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('approve 2 requests when second one exceeds supply cap', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: depositVault, owner }, 0); - await setMaxSupplyCapTest({ depositVault, owner }, 50); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 50, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 50, - ); - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 0 }, { id: 1, expectedToExecute: false }], - 'request-rate', - ); - }); - - it('approve 1 request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - 'request-rate', - ); - }); - - it('approve 2 requests from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVault, 200); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 0 }, { id: 1 }], - 'request-rate', - ); - }); - - it('approve 10 requests from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 1000); - await approveBase18(owner, stableCoins.dai, depositVault, 1000); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - for (let i = 0; i < 10; i++) { - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - } - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - Array.from({ length: 10 }, (_, i) => ({ id: i })), - 'request-rate', - ); - }); - }); - - describe('safeBulkApproveRequest() (custom price overload)', async () => { - it('should fail: call from address without vault admin role', async () => { - const { depositVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = - await loadFixture(defaultDeploy); - await safeBulkApproveRequestTest( - { - depositVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }], - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeBulkApproveRequestTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }], - parseUnits('1'), - { - revertMessage: 'DV: request not exist', - }, - ); - }); - - it('should fail: if new rate greater then variabilityTolerance', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - await safeBulkApproveRequestTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - parseUnits('6'), - { - revertMessage: 'MV: exceed price diviation', - }, - ); - }); - - it('should fail: if new rate lower then variabilityTolerance', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - await safeBulkApproveRequestTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - parseUnits('4'), - { - revertMessage: 'MV: exceed price diviation', - }, - ); - }); - - it('should fail: request already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - parseUnits('5.000001'), - ); - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - parseUnits('5.000001'), - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when one of them already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVault, 200); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - parseUnits('5.000001'), - ); - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0 }], - parseUnits('5.000001'), - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when couple of them have equal id', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVault, 200); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - parseUnits('5.000001'), - ); - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 1 }], - parseUnits('5.000001'), - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('approve 2 requests when second one exceeds supply cap', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: depositVault, owner }, 0); - await setMaxSupplyCapTest({ depositVault, owner }, 50); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 50, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 50, - ); - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 0 }, { id: 1, expectedToExecute: false }], - parseUnits('1'), - ); - }); - - it('approve 1 request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - parseUnits('5.000000001'), - ); - }); - - it('approve 2 requests from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVault, 200); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 0 }, { id: 1 }], - parseUnits('5.000000001'), - ); - }); - - it('approve 10 requests from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 1000); - await approveBase18(owner, stableCoins.dai, depositVault, 1000); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - for (let i = 0; i < 10; i++) { - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - } - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - Array.from({ length: 10 }, (_, i) => ({ id: i })), - parseUnits('5.000000001'), - ); - }); - }); - - describe('safeBulkApproveRequest() (current price overload)', async () => { - it('should fail: call from address without vault admin role', async () => { - const { depositVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = - await loadFixture(defaultDeploy); - await safeBulkApproveRequestTest( - { - depositVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }], - undefined, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeBulkApproveRequestTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }], - undefined, - { - revertMessage: 'DV: request not exist', - }, - ); - }); - - it('should fail: if new rate greater then variabilityTolerance', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 10); - - const requestId = 0; - await safeBulkApproveRequestTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - undefined, - { - revertMessage: 'MV: exceed price diviation', - }, - ); - }); - - it('should fail: if new rate lower then variabilityTolerance', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 3); - - const requestId = 0; - await safeBulkApproveRequestTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - undefined, - { - revertMessage: 'MV: exceed price diviation', - }, - ); - }); - - it('should fail: request already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - undefined, - ); - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - undefined, - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when one of them already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVault, 200); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 0 }], - undefined, - ); - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0 }], - undefined, - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when couple of the have equal id', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVault, 200); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 0 }], - undefined, - ); - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 1 }], - undefined, - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('approve 1 request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - undefined, - ); - }); - - it('approve 1 request from vaut admin account when growth is applied', async () => { - const { - owner, - mockedAggregator, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - customFeedGrowth, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, 5); - await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - undefined, - ); - }); - - it('approve 2 requests from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVault, 200); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 0 }, { id: 1 }], - undefined, - ); - }); - - it('approve 10 requests from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 1000); - await approveBase18(owner, stableCoins.dai, depositVault, 1000); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - for (let i = 0; i < 10; i++) { - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - } - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - Array.from({ length: 10 }, (_, i) => ({ id: i })), - undefined, - ); - }); - - it('approve 10 requests from vaut admin account when different users are recievers', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 1000); - await approveBase18(owner, stableCoins.dai, depositVault, 1000); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - for (let i = 0; i < 10; i++) { - await depositRequestTest( - { - depositVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[i], - }, - stableCoins.dai, - 100, - ); - } - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - Array.from({ length: 10 }, (_, i) => ({ id: i })), - undefined, - ); - }); - - it('approve 2 requests from vaut admin account when each request has different token', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await approveBase18(owner, stableCoins.usdc, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 100, - ); - - await safeBulkApproveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 0 }, { id: 1 }], - undefined, - ); - }); - }); - - describe('rejectRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { depositVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = - await loadFixture(defaultDeploy); - await rejectRequestTest( - { - depositVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await rejectRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - revertMessage: 'DV: request not exist', - }, - ); - }); - - it('should fail: request is already rejected', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - ); - - await rejectRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - { - revertMessage: 'DV: request not pending', - }, - ); - }); - - it('reject request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - ); - }); - }); - - describe('depositInstant() complex', () => { - it('should fail: when is paused', async () => { - const { - depositVault, - owner, - mTBILL, - stableCoins, - regularAccounts, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(depositVault); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('is on pause, but admin can use everything', async () => { - const { - depositVault, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(depositVault); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('call for amount == minAmountToDepositTest', async () => { - const { - depositVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 102_000); - await approveBase18(owner, stableCoins.dai, depositVault, 102_000); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountToDepositTest({ depositVault, owner }, 100_000); - await setInstantDailyLimitTest( - { vault: depositVault, owner }, - parseUnits('150000'), - ); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 102_000, - ); - }); - - it('call for amount == minAmountToDepositTest+1, then deposit with amount 100', async () => { - const { - depositVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 103_101); - await approveBase18(owner, stableCoins.dai, depositVault, 103_101); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountToDepositTest({ depositVault, owner }, 100_000); - await setInstantDailyLimitTest( - { vault: depositVault, owner }, - parseUnits('150000'), - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 103_001, - ); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - }); - - it('deposit 100 DAI, when price is 5$, 25 USDC when price is 5.1$, 14 USDT when price is 5.4$', async () => { - const { - owner, - mockedAggregator, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await mintToken(stableCoins.usdc, owner, 125); - await mintToken(stableCoins.usdt, owner, 114); - - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await approveBase18(owner, stableCoins.usdc, depositVault, 125); - await approveBase18(owner, stableCoins.usdt, depositVault, 114); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.04); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await setRoundData({ mockedAggregator }, 1); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 125, - ); - - await setRoundData({ mockedAggregator }, 1.01); - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdt, - 114, - ); - }); - }); - - describe('depositRequest() complex', () => { - it('should fail: when is paused', async () => { - const { - depositVault, - owner, - mTBILL, - stableCoins, - regularAccounts, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(depositVault); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 100, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('is on pause, but admin can use everything', async () => { - const { - depositVault, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(depositVault); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('call for amount == minAmountToDepositTest', async () => { - const { - depositVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 105_000); - await approveBase18(owner, stableCoins.dai, depositVault, 105_000); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountToDepositTest({ depositVault, owner }, 100_000); - await setInstantDailyLimitTest({ vault: depositVault, owner }, 150_000); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 102_000, - ); - const requestId = 0; - - await approveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - parseUnits('5'), - ); - }); - - it('call for amount == minAmountToDepositTest+1, then deposit with amount 1', async () => { - const { - depositVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 105_101); - await approveBase18(owner, stableCoins.dai, depositVault, 105_101); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountToDepositTest({ depositVault, owner }, 100_000); - await setInstantDailyLimitTest({ vault: depositVault, owner }, 150_000); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 102_001, - ); - let requestId = 0; - - await approveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - parseUnits('5'), - ); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - requestId = 1; - - await approveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - parseUnits('5'), - ); - }); - - it('deposit 100 DAI, when price is 5$, 25 USDC when price is 5.1$, 14 USDT when price is 5.4$', async () => { - const { - owner, - mockedAggregator, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await mintToken(stableCoins.usdc, owner, 125); - await mintToken(stableCoins.usdt, owner, 114); - - await approveBase18(owner, stableCoins.dai, depositVault, 100); - await approveBase18(owner, stableCoins.usdc, depositVault, 125); - await approveBase18(owner, stableCoins.usdt, depositVault, 114); - - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVault, owner }, 10); - - await setRoundData({ mockedAggregator }, 1.04); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - let requestId = 0; - - await approveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - parseUnits('5'), - ); - - await setRoundData({ mockedAggregator }, 1); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 125, - ); - requestId = 1; - - await approveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - parseUnits('5'), - ); - - await setRoundData({ mockedAggregator }, 1.01); - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdt, - 114, - ); - requestId = 2; - - await approveRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, - requestId, - parseUnits('5'), - ); - }); - - it('when 10 supply cap is left and try to mint request 100', async () => { - const { - owner, - depositVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 190); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVault, - 190, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setMaxSupplyCapTest({ depositVault, owner }, 100); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: depositVault, owner }, 0); - - await depositInstantTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 90, - { - from: regularAccounts[0], - }, - ); - - await depositRequestTest( - { depositVault, owner, mTBILL, mTokenToUsdDataFeed, customRecipient }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - }); - - describe('ManageableVault internal functions', () => { - it('should fail: invalid rounding tokenTransferFromToTester()', async () => { - const { depositVault, stableCoins, owner } = await loadFixture( - defaultDeploy, - ); - - await mintToken(stableCoins.usdc, owner, 1000); - - await approveBase18(owner, stableCoins.usdc, depositVault, 1000); - - await expect( - depositVault.tokenTransferFromToTester( - stableCoins.usdc.address, - owner.address, - depositVault.address, - parseUnits('999.999999999'), - 8, - ), - ).revertedWith('MV: invalid rounding'); - }); - - it('should fail: invalid rounding tokenTransferToUserTester()', async () => { - const { depositVault, stableCoins, owner } = await loadFixture( - defaultDeploy, - ); - - await mintToken(stableCoins.usdc, depositVault, 1000); - - await expect( - depositVault.tokenTransferToUserTester( - stableCoins.usdc.address, - owner.address, - parseUnits('999.999999999'), - 8, - ), - ).revertedWith('MV: invalid rounding'); - }); - }); - - describe('_convertUsdToToken', () => { - it('should fail: when amountUsd == 0', async () => { - const { depositVault } = await loadFixture(defaultDeploy); - - await expect( - depositVault.convertTokenToUsdTest(constants.AddressZero, 0), - ).revertedWith('DV: amount zero'); - }); - - it('should fail: when tokenRate == 0', async () => { - const { depositVault } = await loadFixture(defaultDeploy); - - await depositVault.setOverrideGetTokenRate(true); - await depositVault.setGetTokenRateValue(0); - - await expect( - depositVault.convertTokenToUsdTest(constants.AddressZero, 1), - ).revertedWith('DV: rate zero'); - }); - }); - - describe('_convertUsdToMToken', () => { - it('should fail: when rate == 0', async () => { - const { depositVault } = await loadFixture(defaultDeploy); - - await depositVault.setOverrideGetTokenRate(true); - await depositVault.setGetTokenRateValue(0); - - await expect(depositVault.convertUsdToMTokenTest(1)).revertedWith( - 'DV: rate zero', - ); - }); - }); - - describe('_calcAndValidateDeposit', () => { - it('should fail: when tokenOut is not MANUAL_FULLFILMENT_TOKEN but isFiat = true', async () => { - const { depositVault, stableCoins, owner, dataFeed } = await loadFixture( - defaultDeploy, - ); - - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - parseUnits('100', 2), - true, - ); - - await setMinAmountTest({ vault: depositVault, owner }, 0); - - await expect( - depositVault.calcAndValidateDeposit( - constants.AddressZero, - stableCoins.dai.address, - parseUnits('100'), - true, - ), - ).revertedWith('DV: invalid mint amount'); - }); - }); -}); + }, +); diff --git a/test/unit/DepositVaultWithAave.test.ts b/test/unit/DepositVaultWithAave.test.ts index c7b47a55..4a4503df 100644 --- a/test/unit/DepositVaultWithAave.test.ts +++ b/test/unit/DepositVaultWithAave.test.ts @@ -1,19 +1,23 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { constants } from 'ethers'; -import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; -import { encodeFnSelector } from '../../helpers/utils'; -import { ManageableVaultTester__factory } from '../../typechain-types'; -import { acErrors, blackList, greenList } from '../common/ac.helpers'; +import { + baseInitParamsDv, + depositVaultSuits, +} from './suits/deposit-vault.suits'; + +import { + DepositVaultWithAave__factory, + DepositVaultWithAaveTest__factory, +} from '../../typechain-types'; +import { acErrors } from '../common/ac.helpers'; import { approveBase18, mintToken, - pauseVault, - pauseVaultFn, + validateImplementation, } from '../common/common.helpers'; -import { setRoundData } from '../common/data-feed.helpers'; import { depositInstantWithAaveTest, depositRequestWithAaveTest, @@ -22,2654 +26,708 @@ import { setAavePoolTest, setAutoInvestFallbackEnabledAaveTest, } from '../common/deposit-vault-aave.helpers'; -import { - approveRequestTest, - depositRequestTest, - rejectRequestTest, - safeApproveRequestTest, - safeBulkApproveRequestTest, -} from '../common/deposit-vault.helpers'; import { defaultDeploy } from '../common/fixtures'; import { addPaymentTokenTest, - addWaivedFeeAccountTest, - changeTokenAllowanceTest, - removePaymentTokenTest, - removeWaivedFeeAccountTest, - setInstantFeeTest, - setInstantDailyLimitTest, - setMinAmountToDepositTest, setMinAmountTest, - setVariabilityToleranceTest, - withdrawTest, - changeTokenFeeTest, } from '../common/manageable-vault.helpers'; -import { sanctionUser } from '../common/with-sanctions-list.helpers'; - -describe('DepositVaultWithAave', function () { - it('deployment', async () => { - const { - depositVaultWithAave, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - roles, - aavePoolMock, - stableCoins, - } = await loadFixture(defaultDeploy); - - expect(await depositVaultWithAave.mToken()).eq(mTBILL.address); - expect(await depositVaultWithAave.paused()).eq(false); - expect(await depositVaultWithAave.tokensReceiver()).eq( - tokensReceiver.address, - ); - expect(await depositVaultWithAave.feeReceiver()).eq(feeReceiver.address); - expect(await depositVaultWithAave.ONE_HUNDRED_PERCENT()).eq('10000'); - expect(await depositVaultWithAave.minMTokenAmountForFirstDeposit()).eq('0'); - expect(await depositVaultWithAave.minAmount()).eq(parseUnits('100')); - expect(await depositVaultWithAave.instantFee()).eq('100'); - expect(await depositVaultWithAave.instantDailyLimit()).eq( - parseUnits('100000'), - ); - expect(await depositVaultWithAave.mTokenDataFeed()).eq( - mTokenToUsdDataFeed.address, - ); - expect(await depositVaultWithAave.variationTolerance()).eq(1); - expect(await depositVaultWithAave.vaultRole()).eq( - roles.tokenRoles.mTBILL.depositVaultAdmin, - ); - expect(await depositVaultWithAave.MANUAL_FULLFILMENT_TOKEN()).eq( - ethers.constants.AddressZero, - ); +import { initializeDvWithAave } from '../common/vault-initializer.helpers'; + +depositVaultSuits( + 'DepositVaultWithAave', + defaultDeploy, + { + createNew: async (owner: SignerWithAddress) => + new DepositVaultWithAaveTest__factory(owner).deploy(), + key: 'depositVaultWithAave', + }, + async (fixture) => { + const { depositVaultWithAave, stableCoins, aavePoolMock } = fixture; expect(await depositVaultWithAave.aavePools(stableCoins.usdc.address)).eq( aavePoolMock.address, ); expect(await depositVaultWithAave.aaveDepositsEnabled()).eq(false); - }); - - describe('initialization', () => { - it('should fail: call initialize() when already initialized', async () => { - const { depositVaultWithAave } = await loadFixture(defaultDeploy); - - await expect( - depositVaultWithAave.initialize( - constants.AddressZero, - { - mToken: constants.AddressZero, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: constants.AddressZero, - tokensReceiver: constants.AddressZero, - }, - { - instantFee: 0, - instantDailyLimit: 0, - }, - constants.AddressZero, - 0, - 0, - 0, - 0, - ), - ).revertedWith('Initializable: contract is already initialized'); - }); - - it('should fail: call with initializing == false', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initializeWithoutInitializer( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - ), - ).revertedWith('Initializable: contract is not initializing'); - }); - }); - - describe('setAavePool()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - depositVaultWithAave, - owner, - regularAccounts, - stableCoins, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await setAavePoolTest( - { depositVaultWithAave, owner }, - stableCoins.usdc.address, - aavePoolMock.address, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: zero address', async () => { - const { depositVaultWithAave, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - - await setAavePoolTest( - { depositVaultWithAave, owner }, - stableCoins.usdc.address, - ethers.constants.AddressZero, - { - revertMessage: 'zero address', - }, - ); - }); - - it('should fail: token not in pool', async () => { - const { depositVaultWithAave, owner, stableCoins, aavePoolMock } = - await loadFixture(defaultDeploy); - - await setAavePoolTest( - { depositVaultWithAave, owner }, - stableCoins.dai.address, - aavePoolMock.address, - { - revertMessage: 'DVA: token not in pool', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner, stableCoins, aavePoolMock } = - await loadFixture(defaultDeploy); - - await setAavePoolTest( - { depositVaultWithAave, owner }, - stableCoins.usdc.address, - aavePoolMock.address, - ); - }); - }); - - describe('removeAavePool()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner, regularAccounts, stableCoins } = - await loadFixture(defaultDeploy); - - await removeAavePoolTest( - { depositVaultWithAave, owner }, - stableCoins.usdc.address, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: pool not set', async () => { - const { depositVaultWithAave, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - - await removeAavePoolTest( - { depositVaultWithAave, owner }, - stableCoins.dai.address, - { - revertMessage: 'DVA: pool not set', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner, stableCoins, aavePoolMock } = - await loadFixture(defaultDeploy); - - await setAavePoolTest( - { depositVaultWithAave, owner }, - stableCoins.usdc.address, - aavePoolMock.address, - ); - - await removeAavePoolTest( - { depositVaultWithAave, owner }, - stableCoins.usdc.address, - ); - }); - }); - - describe('setAaveDepositsEnabled()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner, regularAccounts } = - await loadFixture(defaultDeploy); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true, { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', + await validateImplementation(DepositVaultWithAave__factory); + }, + { + deployUninitialized: (fixture) => + new DepositVaultWithAaveTest__factory(fixture.owner).deploy(), + initialize: async (fixture, params, opt) => { + await initializeDvWithAave( + { ...baseInitParamsDv(fixture), ...params }, + opt?.contract, + opt, + ); + }, + }, + async (defaultDeploy) => { + describe('DepositVaultWithAave', () => { + describe('setAavePool()', () => { + it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { + depositVaultWithAave, + owner, + regularAccounts, + stableCoins, + aavePoolMock, + } = await loadFixture(defaultDeploy); + + await setAavePoolTest( + { depositVaultWithAave, owner }, + stableCoins.usdc.address, + aavePoolMock.address, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: zero address', async () => { + const { depositVaultWithAave, owner, stableCoins } = + await loadFixture(defaultDeploy); + + await setAavePoolTest( + { depositVaultWithAave, owner }, + stableCoins.usdc.address, + ethers.constants.AddressZero, + { + revertCustomError: { + customErrorName: 'InvalidAddress', + }, + }, + ); + }); + + it('should fail: token not in pool', async () => { + const { depositVaultWithAave, owner, stableCoins, aavePoolMock } = + await loadFixture(defaultDeploy); + + await setAavePoolTest( + { depositVaultWithAave, owner }, + stableCoins.dai.address, + aavePoolMock.address, + { + revertCustomError: { + customErrorName: 'TokenNotInPool', + }, + }, + ); + }); + + it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithAave, owner, stableCoins, aavePoolMock } = + await loadFixture(defaultDeploy); + + await setAavePoolTest( + { depositVaultWithAave, owner }, + stableCoins.usdc.address, + aavePoolMock.address, + ); + }); }); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner } = await loadFixture(defaultDeploy); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true); - }); - - it('toggle on and off', async () => { - const { depositVaultWithAave, owner } = await loadFixture(defaultDeploy); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, false); - }); - }); - - describe('setAutoInvestFallbackEnabled()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner, regularAccounts } = - await loadFixture(defaultDeploy); - - await setAutoInvestFallbackEnabledAaveTest( - { depositVaultWithAave, owner }, - true, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner } = await loadFixture(defaultDeploy); - - await setAutoInvestFallbackEnabledAaveTest( - { depositVaultWithAave, owner }, - true, - ); - }); - - it('toggle on and off', async () => { - const { depositVaultWithAave, owner } = await loadFixture(defaultDeploy); - await setAutoInvestFallbackEnabledAaveTest( - { depositVaultWithAave, owner }, - true, - ); - - await setAutoInvestFallbackEnabledAaveTest( - { depositVaultWithAave, owner }, - false, - ); - }); - }); - - describe('setMinAmount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10, { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', + describe('removeAavePool()', () => { + it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithAave, owner, regularAccounts, stableCoins } = + await loadFixture(defaultDeploy); + + await removeAavePoolTest( + { depositVaultWithAave, owner }, + stableCoins.usdc.address, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: pool not set', async () => { + const { depositVaultWithAave, owner, stableCoins } = + await loadFixture(defaultDeploy); + + await removeAavePoolTest( + { depositVaultWithAave, owner }, + stableCoins.dai.address, + { + revertCustomError: { + customErrorName: 'PoolNotSet', + }, + }, + ); + }); + + it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithAave, owner, stableCoins, aavePoolMock } = + await loadFixture(defaultDeploy); + + await setAavePoolTest( + { depositVaultWithAave, owner }, + stableCoins.usdc.address, + aavePoolMock.address, + ); + + await removeAavePoolTest( + { depositVaultWithAave, owner }, + stableCoins.usdc.address, + ); + }); }); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner } = await loadFixture(defaultDeploy); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - }); - }); - - describe('setMinMTokenAmountForFirstDeposit()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, regularAccounts, owner } = - await loadFixture(defaultDeploy); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithAave, owner }, - 10, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner } = await loadFixture(defaultDeploy); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithAave, owner }, - 10, - ); - }); - }); - - describe('setVariabilityTolerance()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, regularAccounts, owner } = - await loadFixture(defaultDeploy); - - await setVariabilityToleranceTest( - { vault: depositVaultWithAave, owner }, - 100, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner } = await loadFixture(defaultDeploy); - - await setVariabilityToleranceTest( - { vault: depositVaultWithAave, owner }, - 100, - ); - }); - }); - - describe('setInstantDailyLimit()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setInstantDailyLimitTest( - { vault: depositVaultWithAave, owner }, - 10, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner } = await loadFixture(defaultDeploy); - await setInstantDailyLimitTest( - { vault: depositVaultWithAave, owner }, - 10, - ); - }); - }); - - describe('addPaymentToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - depositVaultWithAave, - regularAccounts, - owner, - stableCoins, - dataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - constants.MaxUint256, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - }); - }); - - describe('removePaymentToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, regularAccounts, owner, stableCoins } = - await loadFixture(defaultDeploy); - await removePaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await removePaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - ); - }); - }); - - describe('addWaivedFeeAccount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVaultWithAave, owner }, - regularAccounts[0].address, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVaultWithAave, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('removeWaivedFeeAccount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await removeWaivedFeeAccountTest( - { vault: depositVaultWithAave, owner }, - regularAccounts[0].address, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVaultWithAave, owner }, - regularAccounts[0].address, - ); - await removeWaivedFeeAccountTest( - { vault: depositVaultWithAave, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('setFee()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setInstantFeeTest({ vault: depositVaultWithAave, owner }, 100, { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', + describe('setAaveDepositsEnabled()', () => { + it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithAave, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + await setAaveDepositsEnabledTest( + { depositVaultWithAave, owner }, + true, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithAave, owner } = await loadFixture( + defaultDeploy, + ); + + await setAaveDepositsEnabledTest( + { depositVaultWithAave, owner }, + true, + ); + }); + + it('toggle on and off', async () => { + const { depositVaultWithAave, owner } = await loadFixture( + defaultDeploy, + ); + + await setAaveDepositsEnabledTest( + { depositVaultWithAave, owner }, + true, + ); + + await setAaveDepositsEnabledTest( + { depositVaultWithAave, owner }, + false, + ); + }); }); - }); - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner } = await loadFixture(defaultDeploy); - await setInstantFeeTest({ vault: depositVaultWithAave, owner }, 100); - }); - }); - - describe('withdrawToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, regularAccounts, owner, stableCoins } = - await loadFixture(defaultDeploy); - await withdrawTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc.address, - 0, - owner, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - await mintToken(stableCoins.usdc, depositVaultWithAave, 1); - await withdrawTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - 1, - owner, - ); - }); - }); - - describe('changeTokenAllowance()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, regularAccounts, owner, stableCoins } = - await loadFixture(defaultDeploy); - await changeTokenAllowanceTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc.address, - 100, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc.address, - parseUnits('200'), - ); - }); - }); - - describe('changeTokenFee()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, regularAccounts, owner, stableCoins } = - await loadFixture(defaultDeploy); - await changeTokenFeeTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc.address, - 100, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithAave, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc.address, - 100, - ); - }); - }); - - describe('freeFromMinAmount()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { depositVaultWithAave, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVaultWithAave - .connect(regularAccounts[0]) - .freeFromMinAmount(regularAccounts[1].address, true), - ).to.be.revertedWith('WMAC: hasnt role'); - }); - it('should not fail', async () => { - const { depositVaultWithAave, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVaultWithAave.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.not.reverted; - - expect( - await depositVaultWithAave.isFreeFromMinAmount( - regularAccounts[0].address, - ), - ).to.eq(true); - }); - it('should fail: already in list', async () => { - const { depositVaultWithAave, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVaultWithAave.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.not.reverted; - - expect( - await depositVaultWithAave.isFreeFromMinAmount( - regularAccounts[0].address, - ), - ).to.eq(true); - - await expect( - depositVaultWithAave.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.revertedWith('DV: already free'); - }); - }); - - describe('depositInstant()', async () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await pauseVault(depositVaultWithAave, { - from: owner, + describe('setAutoInvestFallbackEnabled()', () => { + it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithAave, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + await setAutoInvestFallbackEnabledAaveTest( + { depositVaultWithAave, owner }, + true, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithAave, owner } = await loadFixture( + defaultDeploy, + ); + + await setAutoInvestFallbackEnabledAaveTest( + { depositVaultWithAave, owner }, + true, + ); + }); + + it('toggle on and off', async () => { + const { depositVaultWithAave, owner } = await loadFixture( + defaultDeploy, + ); + + await setAutoInvestFallbackEnabledAaveTest( + { depositVaultWithAave, owner }, + true, + ); + + await setAutoInvestFallbackEnabledAaveTest( + { depositVaultWithAave, owner }, + false, + ); + }); }); - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('should fail: user in blacklist', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - accessControl, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await blackList( - { blacklistable: depositVaultWithAave, accessControl, owner }, - regularAccounts[0], - ); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - mockedSanctionsList, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: when trying to deposit 0 amount', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.dai, - 0, - { - revertMessage: 'DV: invalid amount', - }, - ); - }); - - it('should fail: when rounding is invalid', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.dai, - 100.0000000001, - { - revertMessage: 'MV: invalid rounding', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, depositVaultWithAave, 100); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mTokenToUsdDataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, depositVaultWithAave, 10); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(stableCoins.dai, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.dai, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minAmountToDepositTest', async () => { - const { - depositVaultWithAave, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 100_000); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithAave, - 100_000, - ); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithAave, owner }, - 100_000, - ); - await setInstantDailyLimitTest( - { vault: depositVaultWithAave, owner }, - 150_000, - ); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'DV: mint amount < min', - }, - ); - }); - - it('should fail: call for amount < minAmount', async () => { - const { - depositVaultWithAave, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 100_000); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithAave, - 100_000, - ); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithAave, owner }, - 100_000, - ); - await setInstantDailyLimitTest( - { vault: depositVaultWithAave, owner }, - 150_000, - ); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.dai, - 99, - { - revertMessage: 'DV: mToken amount < min', - }, - ); - }); - - it('should fail: if exceed allowance of deposit for token', async () => { - const { - depositVaultWithAave, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.dai, owner, 100_000); - await changeTokenAllowanceTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai.address, - 100, - ); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithAave, - 100_000, - ); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - - it('should fail: if mint limit exceeded', async () => { - const { - depositVaultWithAave, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.dai, owner, 100_000); - await setInstantDailyLimitTest( - { vault: depositVaultWithAave, owner }, - 1000, - ); - - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithAave, - 100_000, - ); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed limit', - }, - ); - }); - - it('should fail: if min receive amount greater then actual', async () => { - const { - depositVaultWithAave, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.dai, owner, 100_000); - - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithAave, - 100_000, - ); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - minAmount: parseUnits('100000'), - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'DV: minReceiveAmount > actual', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithAave, 100); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 10000, - true, - ); - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.dai, - 100, - { - revertMessage: 'DV: mToken amount < min', - }, - ); - - await removePaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: depositVaultWithAave, owner }, 10000); - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.dai, - 100, - { revertMessage: 'DV: mToken amount < min' }, - ); - }); - - it('deposit 100 USDC when aaveDepositsEnabled is true', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('when aaveDepositsEnabled is false, normal DV flow', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - expectedAaveDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit with waived fee', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true); - - await addWaivedFeeAccountTest( - { vault: depositVaultWithAave, owner }, - regularAccounts[0].address, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - waivedFee: true, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI with aave enabled (non-stablecoin feed)', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - aavePoolMock, - aUSDC, - } = await loadFixture(defaultDeploy); - - const aDAI = aUSDC; // reuse the aToken mock for DAI in tests - await aavePoolMock.setReserveAToken( - stableCoins.dai.address, - aDAI.address, - ); - - await setAavePoolTest( - { depositVaultWithAave, owner }, - stableCoins.dai.address, - aavePoolMock.address, - ); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit with greenlist enabled and user in greenlist', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await depositVaultWithAave.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit with custom recipient, aaveDepositsEnabled is true', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - customRecipient: regularAccounts[1], - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit with custom recipient, aaveDepositsEnabled is false', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - expectedAaveDeposited: false, - customRecipient: regularAccounts[1], - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await depositVaultWithAave.setGreenlistEnable(true); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('should fail: first deposit mint amount below configured minimum', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 0); - await setMinAmountToDepositTest( - { depositVault: depositVaultWithAave, owner }, - 200, - ); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithAave, 100); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'DV: mint amount < min', - }, - ); - }); - - it('aaveDepositsEnabled but no pool for token: fallback to normal flow', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - aavePoolMock, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - ); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - expectedAaveDeposited: false, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('aaveDepositsEnabled, pool configured but supply reverts, fallback enabled: fallback to normal flow', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - aavePoolMock, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true); - await setAutoInvestFallbackEnabledAaveTest( - { depositVaultWithAave, owner }, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await aavePoolMock.setShouldRevertSupply(true); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - expectedAaveDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('should fail: aaveDepositsEnabled, pool configured but supply reverts, fallback disabled', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - aavePoolMock, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await aavePoolMock.setShouldRevertSupply(true); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - expectedAaveDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'DVA: auto-invest failed', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await depositVaultWithAave.setGreenlistEnable(true); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - customRecipient, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: when function paused (custom recipient overload)', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - aavePoolMock, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'depositInstant(address,uint256,uint256,bytes32,address)', - ); - await pauseVaultFn(depositVaultWithAave, selector); - await depositInstantWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - }); - - describe('depositRequest()', () => { - it('deposit request 100 USDC', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit request 100 USDC with aave auto-invest', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await depositRequestWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit request with aave auto-invest, supply reverts, fallback enabled: fallback to normal flow', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true); - await setAutoInvestFallbackEnabledAaveTest( - { depositVaultWithAave, owner }, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await aavePoolMock.setShouldRevertSupply(true); - - await depositRequestWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - expectedAaveDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('should fail: deposit request with aave auto-invest, supply reverts, fallback disabled', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await setAaveDepositsEnabledTest({ depositVaultWithAave, owner }, true); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await aavePoolMock.setShouldRevertSupply(true); - - await depositRequestWithAaveTest( - { - depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - aavePoolMock, - expectedAaveDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'DVA: auto-invest failed', - }, - ); - }); - }); - - describe('approveRequest()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - owner, - regularAccounts, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithAave, 100); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await approveRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - request.rate!, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('approve request: happy path', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithAave, 100); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await approveRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - request.rate!, - ); - }); - }); - - describe('safeApproveRequest()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - owner, - regularAccounts, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithAave, 100); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await safeApproveRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - request.rate!, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('safe approve request: happy path', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithAave, 100); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await safeApproveRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - request.rate!, - ); - }); - }); - - describe('safeBulkApproveRequest()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - owner, - regularAccounts, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithAave, 100); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: request.requestId! }], - request.rate!, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('safe bulk approve request: happy path', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithAave, 100); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: request.requestId! }], - request.rate!, - ); - }); - }); - - describe('rejectRequest()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - owner, - regularAccounts, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithAave, 100); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await rejectRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('reject request: happy path', async () => { - const { - owner, - depositVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithAave, 100); - await addPaymentTokenTest( - { vault: depositVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); + describe('depositInstant()', () => { + it('deposit 100 USDC when aaveDepositsEnabled is true', async () => { + const { + owner, + depositVaultWithAave, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + aavePoolMock, + } = await loadFixture(defaultDeploy); + + await setAaveDepositsEnabledTest( + { depositVaultWithAave, owner }, + true, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithAave, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); + + await depositInstantWithAaveTest( + { + depositVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + aavePoolMock, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when aaveDepositsEnabled is false, normal DV flow', async () => { + const { + owner, + depositVaultWithAave, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + aavePoolMock, + } = await loadFixture(defaultDeploy); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithAave, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); + + await depositInstantWithAaveTest( + { + depositVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + aavePoolMock, + expectedAaveDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit with custom recipient, aaveDepositsEnabled is true', async () => { + const { + owner, + depositVaultWithAave, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + aavePoolMock, + } = await loadFixture(defaultDeploy); + + await setAaveDepositsEnabledTest( + { depositVaultWithAave, owner }, + true, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithAave, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); + + await depositInstantWithAaveTest( + { + depositVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + aavePoolMock, + customRecipient: regularAccounts[1], + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('aaveDepositsEnabled but no pool for token: fallback to normal flow', async () => { + const { + owner, + depositVaultWithAave, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + aavePoolMock, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await setAaveDepositsEnabledTest( + { depositVaultWithAave, owner }, + true, + ); + await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVaultWithAave, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithAave, owner }, + stableCoins.dai, + dataFeed.address, + 0, + false, + ); + + await depositInstantWithAaveTest( + { + depositVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + aavePoolMock, + expectedAaveDeposited: false, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('aaveDepositsEnabled, pool configured but supply reverts, fallback enabled: fallback to normal flow', async () => { + const { + owner, + depositVaultWithAave, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + aavePoolMock, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await setAaveDepositsEnabledTest( + { depositVaultWithAave, owner }, + true, + ); + await setAutoInvestFallbackEnabledAaveTest( + { depositVaultWithAave, owner }, + true, + ); + await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithAave, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + await aavePoolMock.setShouldRevertSupply(true); + + await depositInstantWithAaveTest( + { + depositVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + aavePoolMock, + expectedAaveDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('should fail: aaveDepositsEnabled, pool configured but supply reverts, fallback disabled', async () => { + const { + owner, + depositVaultWithAave, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + aavePoolMock, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await setAaveDepositsEnabledTest( + { depositVaultWithAave, owner }, + true, + ); + await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithAave, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + await aavePoolMock.setShouldRevertSupply(true); + + await depositInstantWithAaveTest( + { + depositVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + aavePoolMock, + expectedAaveDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'AutoInvestFailed', + }, + }, + ); + }); + }); - await rejectRequestTest( - { - depositVault: depositVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - ); + describe('depositRequest()', () => { + it('deposit request 100 USDC with aave auto-invest', async () => { + const { + owner, + depositVaultWithAave, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + aavePoolMock, + } = await loadFixture(defaultDeploy); + + await setAaveDepositsEnabledTest( + { depositVaultWithAave, owner }, + true, + ); + await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithAave, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + await depositRequestWithAaveTest( + { + depositVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + aavePoolMock, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit request with aave auto-invest, supply reverts, fallback enabled: fallback to normal flow', async () => { + const { + owner, + depositVaultWithAave, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + aavePoolMock, + } = await loadFixture(defaultDeploy); + + await setAaveDepositsEnabledTest( + { depositVaultWithAave, owner }, + true, + ); + await setAutoInvestFallbackEnabledAaveTest( + { depositVaultWithAave, owner }, + true, + ); + await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithAave, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + await aavePoolMock.setShouldRevertSupply(true); + + await depositRequestWithAaveTest( + { + depositVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + aavePoolMock, + expectedAaveDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('should fail: deposit request with aave auto-invest, supply reverts, fallback disabled', async () => { + const { + owner, + depositVaultWithAave, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + aavePoolMock, + } = await loadFixture(defaultDeploy); + + await setAaveDepositsEnabledTest( + { depositVaultWithAave, owner }, + true, + ); + await setMinAmountTest({ vault: depositVaultWithAave, owner }, 10); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithAave, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + await aavePoolMock.setShouldRevertSupply(true); + + await depositRequestWithAaveTest( + { + depositVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + aavePoolMock, + expectedAaveDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'AutoInvestFailed', + }, + }, + ); + }); + }); }); - }); -}); + }, +); diff --git a/test/unit/DepositVaultWithMToken.test.ts b/test/unit/DepositVaultWithMToken.test.ts index 788e6f4a..1d2b0437 100644 --- a/test/unit/DepositVaultWithMToken.test.ts +++ b/test/unit/DepositVaultWithMToken.test.ts @@ -1,19 +1,26 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; import { constants } from 'ethers'; -import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; -import { encodeFnSelector } from '../../helpers/utils'; -import { ManageableVaultTester__factory } from '../../typechain-types'; -import { acErrors, blackList, greenList } from '../common/ac.helpers'; +import { + baseInitParamsDv, + depositVaultSuits, +} from './suits/deposit-vault.suits'; + +import { + DepositVaultWithMToken__factory, + DepositVaultWithMTokenTest, + DepositVaultWithMTokenTest__factory, +} from '../../typechain-types'; +import { acErrors } from '../common/ac.helpers'; import { approveBase18, + InitializeParamCase, mintToken, - pauseVault, - pauseVaultFn, + validateImplementation, } from '../common/common.helpers'; -import { setRoundData } from '../common/data-feed.helpers'; import { depositInstantWithMTokenTest, depositRequestWithMTokenTest, @@ -21,2628 +28,744 @@ import { setMTokenDepositsEnabledTest, setMTokenDepositVaultTest, } from '../common/deposit-vault-mtoken.helpers'; -import { - approveRequestTest, - depositRequestTest, - rejectRequestTest, - safeApproveRequestTest, - safeBulkApproveRequestTest, -} from '../common/deposit-vault.helpers'; import { defaultDeploy } from '../common/fixtures'; import { addPaymentTokenTest, - addWaivedFeeAccountTest, - changeTokenAllowanceTest, - removePaymentTokenTest, - removeWaivedFeeAccountTest, - setInstantFeeTest, - setInstantDailyLimitTest, - setMinAmountToDepositTest, setMinAmountTest, - setVariabilityToleranceTest, - withdrawTest, - changeTokenFeeTest, + setWaivedFeeAccountTest, } from '../common/manageable-vault.helpers'; -import { sanctionUser } from '../common/with-sanctions-list.helpers'; - -describe('DepositVaultWithMToken', function () { - it('deployment', async () => { - const { - depositVaultWithMToken, - depositVault, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - roles, - } = await loadFixture(defaultDeploy); - - expect(await depositVaultWithMToken.mToken()).eq(mTBILL.address); - expect(await depositVaultWithMToken.paused()).eq(false); - expect(await depositVaultWithMToken.tokensReceiver()).eq( - tokensReceiver.address, - ); - expect(await depositVaultWithMToken.feeReceiver()).eq(feeReceiver.address); - expect(await depositVaultWithMToken.ONE_HUNDRED_PERCENT()).eq('10000'); - expect(await depositVaultWithMToken.minMTokenAmountForFirstDeposit()).eq( - '0', - ); - expect(await depositVaultWithMToken.minAmount()).eq(parseUnits('100')); - expect(await depositVaultWithMToken.instantFee()).eq('100'); - expect(await depositVaultWithMToken.instantDailyLimit()).eq( - parseUnits('100000'), - ); - expect(await depositVaultWithMToken.mTokenDataFeed()).eq( - mTokenToUsdDataFeed.address, - ); - expect(await depositVaultWithMToken.variationTolerance()).eq(1); - expect(await depositVaultWithMToken.vaultRole()).eq( - roles.tokenRoles.mTBILL.depositVaultAdmin, - ); - expect(await depositVaultWithMToken.MANUAL_FULLFILMENT_TOKEN()).eq( - ethers.constants.AddressZero, - ); +import { + initializeDvWithMToken, + InitializerParamsDvWithMToken, +} from '../common/vault-initializer.helpers'; + +const baseInitParamsDvWithMToken = ( + fixture: Parameters[0], +): InitializerParamsDvWithMToken => ({ + ...baseInitParamsDv(fixture), + depositVault: fixture.depositVault, +}); + +const dvWithMTokenInitializeParamCases: InitializeParamCase[] = + [ + { + title: 'depositVault is zero address', + params: { depositVault: constants.AddressZero }, + revertCustomError: { + customErrorName: 'InvalidAddress', + args: [constants.AddressZero], + }, + }, + ]; + +depositVaultSuits( + 'DepositVaultWithMToken', + defaultDeploy, + { + createNew: async (owner: SignerWithAddress) => + new DepositVaultWithMTokenTest__factory(owner).deploy(), + key: 'depositVaultWithMToken', + }, + async (fixture) => { + const { depositVaultWithMToken, depositVault } = fixture; expect(await depositVaultWithMToken.mTokenDepositVault()).eq( depositVault.address, ); expect(await depositVaultWithMToken.mTokenDepositsEnabled()).eq(false); - }); - - describe('initialization', () => { - it('should fail: call initialize() when already initialized', async () => { - const { depositVaultWithMToken } = await loadFixture(defaultDeploy); - - await expect( - depositVaultWithMToken[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,uint256,uint256,address)' - ]( - constants.AddressZero, - { - mToken: constants.AddressZero, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: constants.AddressZero, - tokensReceiver: constants.AddressZero, - }, - { - instantFee: 0, - instantDailyLimit: 0, - }, - constants.AddressZero, - 0, - 0, - 0, - 0, - constants.AddressZero, - ), - ).revertedWith('Initializable: contract is already initialized'); - }); - - it('should fail: call with initializing == false', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initializeWithoutInitializer( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - ), - ).revertedWith('Initializable: contract is not initializing'); - }); - }); - - describe('setMTokenDepositVault()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner, regularAccounts, depositVault } = - await loadFixture(defaultDeploy); - - await setMTokenDepositVaultTest( - { depositVaultWithMToken, owner }, - depositVault.address, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: zero address', async () => { - const { depositVaultWithMToken, owner } = await loadFixture( - defaultDeploy, - ); - - await setMTokenDepositVaultTest( - { depositVaultWithMToken, owner }, - ethers.constants.AddressZero, - { - revertMessage: 'zero address', - }, - ); - }); - - it('should fail: already set to same address', async () => { - const { depositVaultWithMToken, owner, depositVault } = await loadFixture( - defaultDeploy, - ); - - await setMTokenDepositVaultTest( - { depositVaultWithMToken, owner }, - depositVault.address, - { - revertMessage: 'DVMT: already set', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner, regularAccounts } = - await loadFixture(defaultDeploy); - - await setMTokenDepositVaultTest( - { depositVaultWithMToken, owner }, - regularAccounts[1].address, - ); - }); - }); - - describe('setMTokenDepositsEnabled()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner, regularAccounts } = - await loadFixture(defaultDeploy); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner } = await loadFixture( - defaultDeploy, - ); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - ); - }); - - it('toggle on and off', async () => { - const { depositVaultWithMToken, owner } = await loadFixture( - defaultDeploy, - ); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - ); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - false, - ); - }); - }); - - describe('setAutoInvestFallbackEnabled()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner, regularAccounts } = - await loadFixture(defaultDeploy); - - await setAutoInvestFallbackEnabledMTokenTest( - { depositVaultWithMToken, owner }, - true, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner } = await loadFixture( - defaultDeploy, - ); - - await setAutoInvestFallbackEnabledMTokenTest( - { depositVaultWithMToken, owner }, - true, - ); - }); - - it('toggle on and off', async () => { - const { depositVaultWithMToken, owner } = await loadFixture( - defaultDeploy, - ); - - await setAutoInvestFallbackEnabledMTokenTest( - { depositVaultWithMToken, owner }, - true, - ); - - await setAutoInvestFallbackEnabledMTokenTest( - { depositVaultWithMToken, owner }, - false, - ); - }); - }); - - describe('setMinAmount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10, { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', + await validateImplementation(DepositVaultWithMToken__factory); + }, + { + deployUninitialized: (fixture) => + new DepositVaultWithMTokenTest__factory(fixture.owner).deploy(), + initialize: async (fixture, params, opt) => { + await initializeDvWithMToken( + { ...baseInitParamsDvWithMToken(fixture), ...params }, + opt?.contract as DepositVaultWithMTokenTest, + opt, + ); + }, + extraParamCases: dvWithMTokenInitializeParamCases, + }, + async (defaultDeploy) => { + describe('DepositVaultWithMToken', function () { + describe('setMTokenDepositVault()', () => { + it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { + depositVaultWithMToken, + owner, + regularAccounts, + depositVault, + } = await loadFixture(defaultDeploy); + + await setMTokenDepositVaultTest( + { depositVaultWithMToken, owner }, + depositVault.address, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('should fail: zero address', async () => { + const { depositVaultWithMToken, owner } = await loadFixture( + defaultDeploy, + ); + + await setMTokenDepositVaultTest( + { depositVaultWithMToken, owner }, + ethers.constants.AddressZero, + { + revertCustomError: { + customErrorName: 'InvalidAddress', + }, + }, + ); + }); + + it('should fail: already set to same address', async () => { + const { depositVaultWithMToken, owner, depositVault } = + await loadFixture(defaultDeploy); + + await setMTokenDepositVaultTest( + { depositVaultWithMToken, owner }, + depositVault.address, + { + revertCustomError: { + customErrorName: 'SameAddressValue', + }, + }, + ); + }); + + it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithMToken, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + await setMTokenDepositVaultTest( + { depositVaultWithMToken, owner }, + regularAccounts[1].address, + ); + }); }); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner } = await loadFixture( - defaultDeploy, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - }); - }); - - describe('setMinMTokenAmountForFirstDeposit()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, regularAccounts, owner } = - await loadFixture(defaultDeploy); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithMToken, owner }, - 10, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner } = await loadFixture( - defaultDeploy, - ); - await setMinAmountToDepositTest( - { depositVault: depositVaultWithMToken, owner }, - 10, - ); - }); - }); - - describe('setVariabilityTolerance()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, regularAccounts, owner } = - await loadFixture(defaultDeploy); - - await setVariabilityToleranceTest( - { vault: depositVaultWithMToken, owner }, - 100, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner } = await loadFixture( - defaultDeploy, - ); - - await setVariabilityToleranceTest( - { vault: depositVaultWithMToken, owner }, - 100, - ); - }); - }); - - describe('setInstantDailyLimit()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setInstantDailyLimitTest( - { vault: depositVaultWithMToken, owner }, - 10, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner } = await loadFixture( - defaultDeploy, - ); - await setInstantDailyLimitTest( - { vault: depositVaultWithMToken, owner }, - 10, - ); - }); - }); - - describe('addPaymentToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - depositVaultWithMToken, - regularAccounts, - owner, - stableCoins, - dataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - constants.MaxUint256, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - }); - }); - - describe('removePaymentToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, regularAccounts, owner, stableCoins } = - await loadFixture(defaultDeploy); - await removePaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await removePaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - ); - }); - }); - - describe('addWaivedFeeAccount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVaultWithMToken, owner }, - regularAccounts[0].address, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVaultWithMToken, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('removeWaivedFeeAccount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await removeWaivedFeeAccountTest( - { vault: depositVaultWithMToken, owner }, - regularAccounts[0].address, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVaultWithMToken, owner }, - regularAccounts[0].address, - ); - await removeWaivedFeeAccountTest( - { vault: depositVaultWithMToken, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('setFee()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setInstantFeeTest({ vault: depositVaultWithMToken, owner }, 100, { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', + describe('setMTokenDepositsEnabled()', () => { + it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithMToken, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + await setMTokenDepositsEnabledTest( + { depositVaultWithMToken, owner }, + true, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithMToken, owner } = await loadFixture( + defaultDeploy, + ); + + await setMTokenDepositsEnabledTest( + { depositVaultWithMToken, owner }, + true, + ); + }); + + it('toggle on and off', async () => { + const { depositVaultWithMToken, owner } = await loadFixture( + defaultDeploy, + ); + + await setMTokenDepositsEnabledTest( + { depositVaultWithMToken, owner }, + true, + ); + + await setMTokenDepositsEnabledTest( + { depositVaultWithMToken, owner }, + false, + ); + }); }); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner } = await loadFixture( - defaultDeploy, - ); - await setInstantFeeTest({ vault: depositVaultWithMToken, owner }, 100); - }); - }); - - describe('withdrawToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, regularAccounts, owner, stableCoins } = - await loadFixture(defaultDeploy); - await withdrawTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc.address, - 0, - owner, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - await mintToken(stableCoins.usdc, depositVaultWithMToken, 1); - await withdrawTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - 1, - owner, - ); - }); - }); - - describe('changeTokenAllowance()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, regularAccounts, owner, stableCoins } = - await loadFixture(defaultDeploy); - await changeTokenAllowanceTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc.address, - 100, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc.address, - parseUnits('200'), - ); - }); - }); - - describe('changeTokenFee()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, regularAccounts, owner, stableCoins } = - await loadFixture(defaultDeploy); - await changeTokenFeeTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc.address, - 100, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMToken, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc.address, - 100, - ); - }); - }); - - describe('freeFromMinAmount()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { depositVaultWithMToken, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVaultWithMToken - .connect(regularAccounts[0]) - .freeFromMinAmount(regularAccounts[1].address, true), - ).to.be.revertedWith('WMAC: hasnt role'); - }); - it('should not fail', async () => { - const { depositVaultWithMToken, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVaultWithMToken.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.not.reverted; - - expect( - await depositVaultWithMToken.isFreeFromMinAmount( - regularAccounts[0].address, - ), - ).to.eq(true); - }); - it('should fail: already in list', async () => { - const { depositVaultWithMToken, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVaultWithMToken.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.not.reverted; - - expect( - await depositVaultWithMToken.isFreeFromMinAmount( - regularAccounts[0].address, - ), - ).to.eq(true); - - await expect( - depositVaultWithMToken.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.revertedWith('DV: already free'); - }); - }); - - describe('depositInstant()', async () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - await pauseVault(depositVaultWithMToken, { - from: owner, + describe('setAutoInvestFallbackEnabled()', () => { + it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithMToken, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + await setAutoInvestFallbackEnabledMTokenTest( + { depositVaultWithMToken, owner }, + true, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithMToken, owner } = await loadFixture( + defaultDeploy, + ); + + await setAutoInvestFallbackEnabledMTokenTest( + { depositVaultWithMToken, owner }, + true, + ); + }); + + it('toggle on and off', async () => { + const { depositVaultWithMToken, owner } = await loadFixture( + defaultDeploy, + ); + + await setAutoInvestFallbackEnabledMTokenTest( + { depositVaultWithMToken, owner }, + true, + ); + + await setAutoInvestFallbackEnabledMTokenTest( + { depositVaultWithMToken, owner }, + false, + ); + }); }); - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('should fail: user in blacklist', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - accessControl, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - await blackList( - { blacklistable: depositVaultWithMToken, accessControl, owner }, - regularAccounts[0], - ); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: when trying to deposit 0 amount', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 0, - { - revertMessage: 'DV: invalid amount', - }, - ); - }); - - it('should fail: when rounding is invalid', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100.0000000001, - { - revertMessage: 'MV: invalid rounding', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, depositVaultWithMToken, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, depositVaultWithMToken, 10); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(stableCoins.dai, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minAmountToDepositTest', async () => { - const { - depositVaultWithMToken, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 100_000); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithMToken, - 100_000, - ); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithMToken, owner }, - 100_000, - ); - await setInstantDailyLimitTest( - { vault: depositVaultWithMToken, owner }, - 150_000, - ); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'DV: mint amount < min', - }, - ); - }); - - it('should fail: call for amount < minAmount', async () => { - const { - depositVaultWithMToken, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 100_000); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithMToken, - 100_000, - ); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithMToken, owner }, - 100_000, - ); - await setInstantDailyLimitTest( - { vault: depositVaultWithMToken, owner }, - 150_000, - ); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99, - { - revertMessage: 'DV: mToken amount < min', - }, - ); - }); - - it('should fail: if exceed allowance of deposit for token', async () => { - const { - depositVaultWithMToken, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.dai, owner, 100_000); - await changeTokenAllowanceTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai.address, - 100, - ); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithMToken, - 100_000, - ); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - - it('should fail: if mint limit exceeded', async () => { - const { - depositVaultWithMToken, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.dai, owner, 100_000); - await setInstantDailyLimitTest( - { vault: depositVaultWithMToken, owner }, - 1000, - ); - - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithMToken, - 100_000, - ); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed limit', - }, - ); - }); - - it('should fail: if min receive amount greater then actual', async () => { - const { - depositVaultWithMToken, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.dai, owner, 100_000); - - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithMToken, - 100_000, - ); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - minAmount: parseUnits('100000'), - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'DV: minReceiveAmount > actual', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithMToken, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 10000, - true, - ); - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'DV: mToken amount < min', - }, - ); - - await removePaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: depositVaultWithMToken, owner }, 10000); - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { revertMessage: 'DV: mToken amount < min' }, - ); - }); - - it('deposit 100 USDC when mTokenDepositsEnabled is true', async () => { - const { - owner, - depositVaultWithMToken, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - await setMinAmountTest({ vault: depositVault, owner }, 0); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('when mTokenDepositsEnabled is false, normal DV flow', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - expectedMTokenDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit with waived fee', async () => { - const { - owner, - depositVaultWithMToken, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - ); - - await addWaivedFeeAccountTest( - { vault: depositVaultWithMToken, owner }, - regularAccounts[0].address, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - await setMinAmountTest({ vault: depositVault, owner }, 0); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - waivedFee: true, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI with mToken enabled (non-stablecoin feed)', async () => { - const { - owner, - depositVaultWithMToken, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - await setMinAmountTest({ vault: depositVault, owner }, 0); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit with greenlist enabled and user in greenlist', async () => { - const { - owner, - depositVaultWithMToken, - depositVault, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await depositVaultWithMToken.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - await setMinAmountTest({ vault: depositVault, owner }, 0); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit with custom recipient, mTokenDepositsEnabled is true', async () => { - const { - owner, - depositVaultWithMToken, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - await setMinAmountTest({ vault: depositVault, owner }, 0); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[1], - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit with custom recipient, mTokenDepositsEnabled is false', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - expectedMTokenDeposited: false, - customRecipient: regularAccounts[1], - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await depositVaultWithMToken.setGreenlistEnable(true); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('should fail: first deposit mint amount below configured minimum', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 0); - await setMinAmountToDepositTest( - { depositVault: depositVaultWithMToken, owner }, - 200, - ); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMToken, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'DV: mint amount < min', - }, - ); - }); - - it('should fail: mToken deposit enabled with token not in target DV', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithMToken, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - ); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'DVMT: auto-invest failed', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await depositVaultWithMToken.setGreenlistEnable(true); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: when function paused (custom recipient overload)', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'depositInstant(address,uint256,uint256,bytes32,address)', - ); - await pauseVaultFn(depositVaultWithMToken, selector); - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('mTokenDepositsEnabled, auto-invest fails, fallback enabled: fallback to normal flow', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - ); - await setAutoInvestFallbackEnabledMTokenTest( - { depositVaultWithMToken, owner }, - true, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - expectedMTokenDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('should fail: mTokenDepositsEnabled, auto-invest fails, fallback disabled', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - await depositInstantWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - expectedMTokenDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'DVMT: auto-invest failed', - }, - ); - }); - }); - - describe('depositRequest()', () => { - it('deposit request 100 USDC', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit request 100 USDC with mToken auto-invest', async () => { - const { - owner, - depositVaultWithMToken, - depositVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - await setMinAmountTest({ vault: depositVault, owner }, 0); - - await depositRequestWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit request with mToken auto-invest fails, fallback enabled: fallback to normal flow', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - ); - await setAutoInvestFallbackEnabledMTokenTest( - { depositVaultWithMToken, owner }, - true, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - await depositRequestWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - expectedMTokenDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('should fail: deposit request with mToken auto-invest fails, fallback disabled', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await setMTokenDepositsEnabledTest( - { depositVaultWithMToken, owner }, - true, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - await depositRequestWithMTokenTest( - { - depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - expectedMTokenDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'DVMT: auto-invest failed', - }, - ); - }); - }); - - describe('approveRequest()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - owner, - regularAccounts, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMToken, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await approveRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - request.rate!, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('approve request: happy path', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMToken, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await approveRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - request.rate!, - ); - }); - }); - - describe('safeApproveRequest()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - owner, - regularAccounts, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMToken, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await safeApproveRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - request.rate!, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('safe approve request: happy path', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMToken, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await safeApproveRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - request.rate!, - ); - }); - }); - - describe('safeBulkApproveRequest()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - owner, - regularAccounts, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMToken, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: request.requestId! }], - request.rate!, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('safe bulk approve request: happy path', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMToken, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: request.requestId! }], - request.rate!, - ); - }); - }); - - describe('rejectRequest()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - owner, - regularAccounts, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMToken, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await rejectRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('reject request: happy path', async () => { - const { - owner, - depositVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMToken, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMToken, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); + describe('depositInstant()', async () => { + it('deposit 100 USDC when mTokenDepositsEnabled is true', async () => { + const { + owner, + depositVaultWithMToken, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadFixture(defaultDeploy); + + await setMTokenDepositsEnabledTest( + { depositVaultWithMToken, owner }, + true, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMToken, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMToken, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositInstantWithMTokenTest( + { + depositVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when mTokenDepositsEnabled is false, normal DV flow', async () => { + const { + owner, + depositVaultWithMToken, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadFixture(defaultDeploy); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMToken, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMToken, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); + + await depositInstantWithMTokenTest( + { + depositVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + expectedMTokenDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit with waived fee', async () => { + const { + owner, + depositVaultWithMToken, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadFixture(defaultDeploy); + + await setMTokenDepositsEnabledTest( + { depositVaultWithMToken, owner }, + true, + ); + + await setWaivedFeeAccountTest( + { vault: depositVaultWithMToken, owner }, + regularAccounts[0].address, + true, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMToken, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMToken, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositInstantWithMTokenTest( + { + depositVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + waivedFee: true, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 DAI with mToken enabled (non-stablecoin feed)', async () => { + const { + owner, + depositVaultWithMToken, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadFixture(defaultDeploy); + + await setMTokenDepositsEnabledTest( + { depositVaultWithMToken, owner }, + true, + ); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVaultWithMToken, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + false, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + false, + ); + await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositInstantWithMTokenTest( + { + depositVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('should fail: mToken deposit enabled with token not in target DV', async () => { + const { + owner, + depositVaultWithMToken, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + } = await loadFixture(defaultDeploy); + + await setMTokenDepositsEnabledTest( + { depositVaultWithMToken, owner }, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18( + owner, + stableCoins.dai, + depositVaultWithMToken, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + false, + ); + + await depositInstantWithMTokenTest( + { + depositVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'AutoInvestFailed', + }, + }, + ); + }); + + it('mTokenDepositsEnabled, auto-invest fails, fallback enabled: fallback to normal flow', async () => { + const { + owner, + depositVaultWithMToken, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadFixture(defaultDeploy); + + await setMTokenDepositsEnabledTest( + { depositVaultWithMToken, owner }, + true, + ); + await setAutoInvestFallbackEnabledMTokenTest( + { depositVaultWithMToken, owner }, + true, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMToken, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMToken, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); + + await depositInstantWithMTokenTest( + { + depositVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + expectedMTokenDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('should fail: mTokenDepositsEnabled, auto-invest fails, fallback disabled', async () => { + const { + owner, + depositVaultWithMToken, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadFixture(defaultDeploy); + + await setMTokenDepositsEnabledTest( + { depositVaultWithMToken, owner }, + true, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMToken, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMToken, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); + + await depositInstantWithMTokenTest( + { + depositVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + expectedMTokenDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'AutoInvestFailed', + }, + }, + ); + }); + }); - await rejectRequestTest( - { - depositVault: depositVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - ); + describe('depositRequest()', () => { + it('deposit request 100 USDC with mToken auto-invest', async () => { + const { + owner, + depositVaultWithMToken, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadFixture(defaultDeploy); + + await setMTokenDepositsEnabledTest( + { depositVaultWithMToken, owner }, + true, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMToken, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMToken, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestWithMTokenTest( + { + depositVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit request with mToken auto-invest fails, fallback enabled: fallback to normal flow', async () => { + const { + owner, + depositVaultWithMToken, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadFixture(defaultDeploy); + + await setMTokenDepositsEnabledTest( + { depositVaultWithMToken, owner }, + true, + ); + await setAutoInvestFallbackEnabledMTokenTest( + { depositVaultWithMToken, owner }, + true, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMToken, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMToken, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); + + await depositRequestWithMTokenTest( + { + depositVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + expectedMTokenDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('should fail: deposit request with mToken auto-invest fails, fallback disabled', async () => { + const { + owner, + depositVaultWithMToken, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadFixture(defaultDeploy); + + await setMTokenDepositsEnabledTest( + { depositVaultWithMToken, owner }, + true, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMToken, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMToken, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMToken, owner }, 10); + + await depositRequestWithMTokenTest( + { + depositVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + expectedMTokenDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'AutoInvestFailed', + }, + }, + ); + }); + }); }); - }); -}); + }, +); diff --git a/test/unit/DepositVaultWithMorpho.test.ts b/test/unit/DepositVaultWithMorpho.test.ts index 7d57e174..3be05234 100644 --- a/test/unit/DepositVaultWithMorpho.test.ts +++ b/test/unit/DepositVaultWithMorpho.test.ts @@ -1,22 +1,25 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { constants } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; -import { encodeFnSelector } from '../../helpers/utils'; import { - ManageableVaultTester__factory, + baseInitParamsDv, + depositVaultSuits, +} from './suits/deposit-vault.suits'; + +import { + DepositVaultWithMorpho__factory, + DepositVaultWithMorphoTest__factory, MorphoVaultMock__factory, } from '../../typechain-types'; -import { acErrors, blackList, greenList } from '../common/ac.helpers'; +import { acErrors } from '../common/ac.helpers'; import { approveBase18, mintToken, - pauseVault, - pauseVaultFn, + validateImplementation, } from '../common/common.helpers'; -import { setRoundData } from '../common/data-feed.helpers'; import { depositInstantWithMorphoTest, depositRequestWithMorphoTest, @@ -25,2795 +28,975 @@ import { setMorphoDepositsEnabledTest, setMorphoVaultTest, } from '../common/deposit-vault-morpho.helpers'; -import { - approveRequestTest, - depositRequestTest, - rejectRequestTest, - safeApproveRequestTest, - safeBulkApproveRequestTest, -} from '../common/deposit-vault.helpers'; import { defaultDeploy } from '../common/fixtures'; import { addPaymentTokenTest, - addWaivedFeeAccountTest, - changeTokenAllowanceTest, - changeTokenFeeTest, - removePaymentTokenTest, - removeWaivedFeeAccountTest, - setInstantDailyLimitTest, - setMinAmountToDepositTest, - setInstantFeeTest, setMinAmountTest, - setVariabilityToleranceTest, - withdrawTest, } from '../common/manageable-vault.helpers'; -import { sanctionUser } from '../common/with-sanctions-list.helpers'; - -describe('DepositVaultWithMorpho', function () { - it('deployment', async () => { - const { - depositVaultWithMorpho, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - roles, - } = await loadFixture(defaultDeploy); - - expect(await depositVaultWithMorpho.mToken()).eq(mTBILL.address); - expect(await depositVaultWithMorpho.paused()).eq(false); - expect(await depositVaultWithMorpho.tokensReceiver()).eq( - tokensReceiver.address, - ); - expect(await depositVaultWithMorpho.feeReceiver()).eq(feeReceiver.address); - expect(await depositVaultWithMorpho.ONE_HUNDRED_PERCENT()).eq('10000'); - expect(await depositVaultWithMorpho.minMTokenAmountForFirstDeposit()).eq( - '0', - ); - expect(await depositVaultWithMorpho.minAmount()).eq(parseUnits('100')); - expect(await depositVaultWithMorpho.instantFee()).eq('100'); - expect(await depositVaultWithMorpho.instantDailyLimit()).eq( - parseUnits('100000'), - ); - expect(await depositVaultWithMorpho.mTokenDataFeed()).eq( - mTokenToUsdDataFeed.address, - ); - expect(await depositVaultWithMorpho.variationTolerance()).eq(1); - expect(await depositVaultWithMorpho.vaultRole()).eq( - roles.tokenRoles.mTBILL.depositVaultAdmin, - ); - expect(await depositVaultWithMorpho.MANUAL_FULLFILMENT_TOKEN()).eq( - ethers.constants.AddressZero, - ); +import { initializeDvWithMorpho } from '../common/vault-initializer.helpers'; + +depositVaultSuits( + 'DepositVaultWithMorpho', + defaultDeploy, + { + createNew: async (owner: SignerWithAddress) => + new DepositVaultWithMorphoTest__factory(owner).deploy(), + key: 'depositVaultWithMorpho', + }, + async (fixture) => { + const { depositVaultWithMorpho } = fixture; expect(await depositVaultWithMorpho.morphoDepositsEnabled()).eq(false); - }); - - describe('initialization', () => { - it('should fail: call initialize() when already initialized', async () => { - const { depositVaultWithMorpho } = await loadFixture(defaultDeploy); - - await expect( - depositVaultWithMorpho.initialize( - constants.AddressZero, - { - mToken: constants.AddressZero, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: constants.AddressZero, - tokensReceiver: constants.AddressZero, - }, - { - instantFee: 0, - instantDailyLimit: 0, - }, - constants.AddressZero, - 0, - 0, - 0, - 0, - ), - ).revertedWith('Initializable: contract is already initialized'); - }); - - it('should fail: call with initializing == false', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initializeWithoutInitializer( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - ), - ).revertedWith('Initializable: contract is not initializing'); - }); - }); - - describe('setMorphoVault()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - depositVaultWithMorpho, - owner, - regularAccounts, - stableCoins, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: zero token address', async () => { - const { depositVaultWithMorpho, owner, morphoVaultMock } = - await loadFixture(defaultDeploy); - - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - ethers.constants.AddressZero, - morphoVaultMock.address, - { - revertMessage: 'zero address', - }, - ); - }); - - it('should fail: zero vault address', async () => { - const { depositVaultWithMorpho, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - ethers.constants.AddressZero, - { - revertMessage: 'zero address', - }, - ); - }); - - it('should fail: asset mismatch', async () => { - const { depositVaultWithMorpho, owner, stableCoins, morphoVaultMock } = - await loadFixture(defaultDeploy); - - // morphoVaultMock is configured for USDC; passing DAI should fail - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.dai.address, - morphoVaultMock.address, - { - revertMessage: 'DVM: asset mismatch', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner, stableCoins, morphoVaultMock } = - await loadFixture(defaultDeploy); - - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - ); - }); - }); - - describe('removeMorphoVault()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner, regularAccounts, stableCoins } = - await loadFixture(defaultDeploy); - - await removeMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: vault not set', async () => { - const { depositVaultWithMorpho, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - - await removeMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - { - revertMessage: 'DVM: vault not set', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner, stableCoins, morphoVaultMock } = - await loadFixture(defaultDeploy); - - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - ); - - await removeMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - ); - }); - }); - - describe('setMorphoDepositsEnabled()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner, regularAccounts } = - await loadFixture(defaultDeploy); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner } = await loadFixture( - defaultDeploy, - ); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - }); - - it('toggle on and off', async () => { - const { depositVaultWithMorpho, owner } = await loadFixture( - defaultDeploy, - ); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - false, - ); - }); - }); - - describe('setAutoInvestFallbackEnabled()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner, regularAccounts } = - await loadFixture(defaultDeploy); - - await setAutoInvestFallbackEnabledMorphoTest( - { depositVaultWithMorpho, owner }, - true, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner } = await loadFixture( - defaultDeploy, - ); - - await setAutoInvestFallbackEnabledMorphoTest( - { depositVaultWithMorpho, owner }, - true, - ); - }); - - it('toggle on and off', async () => { - const { depositVaultWithMorpho, owner } = await loadFixture( - defaultDeploy, - ); - - await setAutoInvestFallbackEnabledMorphoTest( - { depositVaultWithMorpho, owner }, - true, - ); - - await setAutoInvestFallbackEnabledMorphoTest( - { depositVaultWithMorpho, owner }, - false, - ); - }); - }); - - describe('setMinAmount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10, { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', + await validateImplementation(DepositVaultWithMorpho__factory); + }, + { + deployUninitialized: (fixture) => + new DepositVaultWithMorphoTest__factory(fixture.owner).deploy(), + initialize: async (fixture, params, opt) => { + await initializeDvWithMorpho( + { ...baseInitParamsDv(fixture), ...params }, + opt?.contract, + opt, + ); + }, + }, + async (defaultDeploy) => { + describe('DepositVaultWithMorpho', function () { + describe('setMorphoVault()', () => { + it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { + depositVaultWithMorpho, + owner, + regularAccounts, + stableCoins, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('should fail: zero token address', async () => { + const { depositVaultWithMorpho, owner, morphoVaultMock } = + await loadFixture(defaultDeploy); + + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + ethers.constants.AddressZero, + morphoVaultMock.address, + { + revertCustomError: { + customErrorName: 'InvalidAddress', + }, + }, + ); + }); + + it('should fail: zero vault address', async () => { + const { depositVaultWithMorpho, owner, stableCoins } = + await loadFixture(defaultDeploy); + + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + ethers.constants.AddressZero, + { + revertCustomError: { + customErrorName: 'InvalidAddress', + }, + }, + ); + }); + + it('should fail: asset mismatch', async () => { + const { + depositVaultWithMorpho, + owner, + stableCoins, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + // morphoVaultMock is configured for USDC; passing DAI should fail + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.dai.address, + morphoVaultMock.address, + { + revertCustomError: { + customErrorName: 'AssetMismatch', + }, + }, + ); + }); + + it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { + depositVaultWithMorpho, + owner, + stableCoins, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + ); + }); }); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner } = await loadFixture( - defaultDeploy, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - }); - }); - - describe('setMinMTokenAmountForFirstDeposit()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, regularAccounts, owner } = - await loadFixture(defaultDeploy); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithMorpho, owner }, - 10, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner } = await loadFixture( - defaultDeploy, - ); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithMorpho, owner }, - 10, - ); - }); - }); - - describe('setInstantDailyLimit()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, regularAccounts, owner } = - await loadFixture(defaultDeploy); - - await setInstantDailyLimitTest( - { vault: depositVaultWithMorpho, owner }, - 10, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner } = await loadFixture( - defaultDeploy, - ); - - await setInstantDailyLimitTest( - { vault: depositVaultWithMorpho, owner }, - 10, - ); - }); - }); - - describe('setVariabilityTolerance()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, regularAccounts, owner } = - await loadFixture(defaultDeploy); - - await setVariabilityToleranceTest( - { vault: depositVaultWithMorpho, owner }, - 100, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner } = await loadFixture( - defaultDeploy, - ); - - await setVariabilityToleranceTest( - { vault: depositVaultWithMorpho, owner }, - 100, - ); - }); - }); - - describe('addPaymentToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - depositVaultWithMorpho, - regularAccounts, - owner, - stableCoins, - dataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - constants.MaxUint256, - { from: regularAccounts[0], revertMessage: 'WMAC: hasnt role' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - }); - }); - - describe('removePaymentToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, regularAccounts, owner, stableCoins } = - await loadFixture(defaultDeploy); - - await removePaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await removePaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - ); - }); - }); - - describe('addWaivedFeeAccount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVaultWithMorpho, owner }, - regularAccounts[1].address, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVaultWithMorpho, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('removeWaivedFeeAccount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, regularAccounts, owner } = - await loadFixture(defaultDeploy); - - await addWaivedFeeAccountTest( - { vault: depositVaultWithMorpho, owner }, - regularAccounts[1].address, - ); - - await removeWaivedFeeAccountTest( - { vault: depositVaultWithMorpho, owner }, - regularAccounts[1].address, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, regularAccounts, owner } = - await loadFixture(defaultDeploy); - - await addWaivedFeeAccountTest( - { vault: depositVaultWithMorpho, owner }, - regularAccounts[0].address, - ); - await removeWaivedFeeAccountTest( - { vault: depositVaultWithMorpho, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('withdrawToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner, stableCoins, regularAccounts } = - await loadFixture(defaultDeploy); - await mintToken(stableCoins.usdc, depositVaultWithMorpho, 1); - await withdrawTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - 1, - owner, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - await mintToken(stableCoins.usdc, depositVaultWithMorpho, 100); - const usdcDecimals = await stableCoins.usdc.decimals(); - await withdrawTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - parseUnits('100', usdcDecimals), - owner, - ); - }); - }); - - describe('changeTokenAllowance()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner, stableCoins, regularAccounts } = - await loadFixture(defaultDeploy); - await changeTokenAllowanceTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - 100, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - parseUnits('200'), - ); - }); - }); - - describe('changeTokenFee()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner, stableCoins, regularAccounts } = - await loadFixture(defaultDeploy); - await changeTokenFeeTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - 100, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithMorpho, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - 100, - ); - }); - }); - - describe('freeFromMinAmount()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { depositVaultWithMorpho, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVaultWithMorpho - .connect(regularAccounts[0]) - .freeFromMinAmount(regularAccounts[1].address, true), - ).to.be.revertedWith('WMAC: hasnt role'); - }); - it('should not fail', async () => { - const { depositVaultWithMorpho, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVaultWithMorpho.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.not.reverted; - - expect( - await depositVaultWithMorpho.isFreeFromMinAmount( - regularAccounts[0].address, - ), - ).to.eq(true); - }); - it('should fail: already in list', async () => { - const { depositVaultWithMorpho, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVaultWithMorpho.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.not.reverted; - - expect( - await depositVaultWithMorpho.isFreeFromMinAmount( - regularAccounts[0].address, - ), - ).to.eq(true); - - await expect( - depositVaultWithMorpho.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.revertedWith('DV: already free'); - }); - }); - - describe('depositInstant()', async () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await pauseVault(depositVaultWithMorpho, { - from: owner, + describe('removeMorphoVault()', () => { + it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { + depositVaultWithMorpho, + owner, + regularAccounts, + stableCoins, + } = await loadFixture(defaultDeploy); + + await removeMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('should fail: vault not set', async () => { + const { depositVaultWithMorpho, owner, stableCoins } = + await loadFixture(defaultDeploy); + + await removeMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + { + revertCustomError: { + customErrorName: 'VaultNotSet', + }, + }, + ); + }); + + it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { + depositVaultWithMorpho, + owner, + stableCoins, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + ); + + await removeMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + ); + }); }); - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('should fail: user in blacklist', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - accessControl, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await blackList( - { blacklistable: depositVaultWithMorpho, accessControl, owner }, - regularAccounts[0], - ); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: when trying to deposit 0 amount', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 0, - { - revertMessage: 'DV: invalid amount', - }, - ); - }); - - it('should fail: when rounding is invalid', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 100.0000000001, - { - revertMessage: 'MV: invalid rounding', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.usdc, depositVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.usdc, depositVaultWithMorpho, 10); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await mintToken(stableCoins.usdc, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minAmountToDepositTest', async () => { - const { - depositVaultWithMorpho, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.usdc, owner, 100_000); - await approveBase18( - owner, - stableCoins.usdc, - depositVaultWithMorpho, - 100_000, - ); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithMorpho, owner }, - 100_000, - ); - await setInstantDailyLimitTest( - { vault: depositVaultWithMorpho, owner }, - 150_000, - ); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'DV: mint amount < min', - }, - ); - }); - - it('should fail: call for amount < minAmount', async () => { - const { - depositVaultWithMorpho, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.usdc, owner, 100_000); - await approveBase18( - owner, - stableCoins.usdc, - depositVaultWithMorpho, - 100_000, - ); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithMorpho, owner }, - 100_000, - ); - await setInstantDailyLimitTest( - { vault: depositVaultWithMorpho, owner }, - 150_000, - ); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 99, - { - revertMessage: 'DV: mToken amount < min', - }, - ); - }); - - it('should fail: if exceed allowance of deposit for token', async () => { - const { - depositVaultWithMorpho, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.usdc, owner, 100_000); - await changeTokenAllowanceTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - 100, - ); - await approveBase18( - owner, - stableCoins.usdc, - depositVaultWithMorpho, - 100_000, - ); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - - it('should fail: if mint limit exceeded', async () => { - const { - depositVaultWithMorpho, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.usdc, owner, 100_000); - await setInstantDailyLimitTest( - { vault: depositVaultWithMorpho, owner }, - 1000, - ); - - await approveBase18( - owner, - stableCoins.usdc, - depositVaultWithMorpho, - 100_000, - ); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'MV: exceed limit', - }, - ); - }); - - it('should fail: if min receive amount greater then actual', async () => { - const { - depositVaultWithMorpho, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.usdc, owner, 100_000); - - await approveBase18( - owner, - stableCoins.usdc, - depositVaultWithMorpho, - 100_000, - ); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - minAmount: parseUnits('100000'), - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'DV: minReceiveAmount > actual', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 10000, - true, - ); - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'DV: mToken amount < min', - }, - ); - - await removePaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: depositVaultWithMorpho, owner }, 10000); - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 100, - { revertMessage: 'DV: mToken amount < min' }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await depositVaultWithMorpho.setGreenlistEnable(true); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('morphoDepositsEnabled but no vault for token: fallback to normal flow', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - expectedMorphoDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('morphoDepositsEnabled, vault configured but deposit reverts, fallback enabled: fallback to normal flow', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - await setAutoInvestFallbackEnabledMorphoTest( - { depositVaultWithMorpho, owner }, - true, - ); - - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await morphoVaultMock.setShouldRevertDeposit(true); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - expectedMorphoDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('should fail: morphoDepositsEnabled, vault configured but deposit reverts, fallback disabled', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await morphoVaultMock.setShouldRevertDeposit(true); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - expectedMorphoDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'DVM: auto-invest failed', - }, - ); - }); - - it('should fail: when Morpho deposit mints zero shares', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 0); - await morphoVaultMock.setExchangeRate(parseUnits('1000000000000')); - - await mintToken(stableCoins.usdc, owner, 1); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMorpho, 1); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - expectedMorphoDeposited: false, - }, - stableCoins.usdc, - 0.000001, - { - revertMessage: 'DVM: zero shares', - }, - ); - }); - - it('deposit 100 USDC when morphoDepositsEnabled is true', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('when morphoDepositsEnabled is false, normal DV flow', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - expectedMorphoDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit with waived fee', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - ); - - await addWaivedFeeAccountTest( - { vault: depositVaultWithMorpho, owner }, - regularAccounts[0].address, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - waivedFee: true, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit with greenlist enabled and user in greenlist', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await depositVaultWithMorpho.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit with custom recipient, morphoDepositsEnabled is true', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - customRecipient: regularAccounts[1], - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI with morpho enabled (per-asset vault mapping)', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - const daiMorphoVault = await new MorphoVaultMock__factory(owner).deploy( - stableCoins.dai.address, - ); - await stableCoins.dai.mint(daiMorphoVault.address, parseUnits('1000000')); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.dai.address, - daiMorphoVault.address, - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock: daiMorphoVault, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('toggle mid-flight: morpho enabled then disabled', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - ); - - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - // Deposit 1: morpho enabled - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 200); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 200, - ); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - - // Deposit 2: morpho disabled - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - false, - ); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - expectedMorphoDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await depositVaultWithMorpho.setGreenlistEnable(true); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - customRecipient, - }, - stableCoins.usdc, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - customRecipient, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: depositVaultWithMorpho, accessControl, owner }, - customRecipient, - ); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - customRecipient, - }, - stableCoins.usdc, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - customRecipient, - }, - stableCoins.usdc, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: when function paused (custom recipient overload)', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'depositInstant(address,uint256,uint256,bytes32,address)', - ); - await pauseVaultFn(depositVaultWithMorpho, selector); - await depositInstantWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - customRecipient, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - }); - - describe('depositRequest()', () => { - it('deposit request 100 USDC', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit request 100 USDC with morpho auto-invest', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await depositRequestWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit request with morpho auto-invest, deposit reverts, fallback enabled: fallback to normal flow', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - await setAutoInvestFallbackEnabledMorphoTest( - { depositVaultWithMorpho, owner }, - true, - ); - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await morphoVaultMock.setShouldRevertDeposit(true); - - await depositRequestWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - expectedMorphoDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('should fail: deposit request with morpho auto-invest, deposit reverts, fallback disabled', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await setMorphoDepositsEnabledTest( - { depositVaultWithMorpho, owner }, - true, - ); - await setMorphoVaultTest( - { depositVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await morphoVaultMock.setShouldRevertDeposit(true); - - await depositRequestWithMorphoTest( - { - depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - morphoVaultMock, - expectedMorphoDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'DVM: auto-invest failed', - }, - ); - }); - }); - - describe('approveRequest()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - owner, - regularAccounts, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await approveRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - request.rate!, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('approve request: happy path', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await approveRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - request.rate!, - ); - }); - }); - - describe('safeApproveRequest()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - owner, - regularAccounts, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await safeApproveRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - request.rate!, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('safe approve request: happy path', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await safeApproveRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - request.rate!, - ); - }); - }); - - describe('safeBulkApproveRequest()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - owner, - regularAccounts, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: request.requestId! }], - request.rate!, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('safe bulk approve request: happy path', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: request.requestId! }], - request.rate!, - ); - }); - }); - - describe('rejectRequest()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { - owner, - regularAccounts, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); + describe('setMorphoDepositsEnabled()', () => { + it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithMorpho, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + true, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithMorpho, owner } = await loadFixture( + defaultDeploy, + ); + + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + true, + ); + }); + + it('toggle on and off', async () => { + const { depositVaultWithMorpho, owner } = await loadFixture( + defaultDeploy, + ); + + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + true, + ); + + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + false, + ); + }); + }); - await rejectRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); + describe('setAutoInvestFallbackEnabled()', () => { + it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithMorpho, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + await setAutoInvestFallbackEnabledMorphoTest( + { depositVaultWithMorpho, owner }, + true, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { depositVaultWithMorpho, owner } = await loadFixture( + defaultDeploy, + ); + + await setAutoInvestFallbackEnabledMorphoTest( + { depositVaultWithMorpho, owner }, + true, + ); + }); + + it('toggle on and off', async () => { + const { depositVaultWithMorpho, owner } = await loadFixture( + defaultDeploy, + ); + + await setAutoInvestFallbackEnabledMorphoTest( + { depositVaultWithMorpho, owner }, + true, + ); + + await setAutoInvestFallbackEnabledMorphoTest( + { depositVaultWithMorpho, owner }, + false, + ); + }); + }); - it('reject request: happy path', async () => { - const { - owner, - depositVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: depositVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); - - const request = await depositRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); + describe('depositInstant()', async () => { + it('morphoDepositsEnabled but no vault for token: fallback to normal flow', async () => { + const { + owner, + depositVaultWithMorpho, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + true, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMorpho, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); + + await depositInstantWithMorphoTest( + { + depositVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + morphoVaultMock, + expectedMorphoDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('morphoDepositsEnabled, vault configured but deposit reverts, fallback enabled: fallback to normal flow', async () => { + const { + owner, + depositVaultWithMorpho, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + true, + ); + await setAutoInvestFallbackEnabledMorphoTest( + { depositVaultWithMorpho, owner }, + true, + ); + + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMorpho, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); + + await morphoVaultMock.setShouldRevertDeposit(true); + + await depositInstantWithMorphoTest( + { + depositVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + morphoVaultMock, + expectedMorphoDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('should fail: morphoDepositsEnabled, vault configured but deposit reverts, fallback disabled', async () => { + const { + owner, + depositVaultWithMorpho, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + true, + ); + + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMorpho, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); + + await morphoVaultMock.setShouldRevertDeposit(true); + + await depositInstantWithMorphoTest( + { + depositVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + morphoVaultMock, + expectedMorphoDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'AutoInvestFailed', + }, + }, + ); + }); + + it('should fail: when Morpho deposit mints zero shares', async () => { + const { + owner, + depositVaultWithMorpho, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + true, + ); + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + ); + await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 0); + await morphoVaultMock.setExchangeRate(parseUnits('1000000000000')); + + await mintToken(stableCoins.usdc, owner, 1); + await approveBase18( + owner, + stableCoins.usdc, + depositVaultWithMorpho, + 1, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + await depositInstantWithMorphoTest( + { + depositVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + morphoVaultMock, + expectedMorphoDeposited: false, + }, + stableCoins.usdc, + 0.000001, + { + revertCustomError: { + customErrorName: 'ZeroShares', + }, + }, + ); + }); + + it('deposit 100 USDC when morphoDepositsEnabled is true', async () => { + const { + owner, + depositVaultWithMorpho, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + true, + ); + + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMorpho, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); + + await depositInstantWithMorphoTest( + { + depositVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + morphoVaultMock, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when morphoDepositsEnabled is false, normal DV flow', async () => { + const { + owner, + depositVaultWithMorpho, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMorpho, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); + + await depositInstantWithMorphoTest( + { + depositVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + morphoVaultMock, + expectedMorphoDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit with custom recipient, morphoDepositsEnabled is true', async () => { + const { + owner, + depositVaultWithMorpho, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + true, + ); + + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMorpho, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); + + await depositInstantWithMorphoTest( + { + depositVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + morphoVaultMock, + customRecipient: regularAccounts[1], + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 DAI with morpho enabled (per-asset vault mapping)', async () => { + const { + owner, + depositVaultWithMorpho, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadFixture(defaultDeploy); + + const daiMorphoVault = await new MorphoVaultMock__factory( + owner, + ).deploy(stableCoins.dai.address); + await stableCoins.dai.mint( + daiMorphoVault.address, + parseUnits('1000000'), + ); + + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + true, + ); + + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.dai.address, + daiMorphoVault.address, + ); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVaultWithMorpho, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMorpho, owner }, + stableCoins.dai, + dataFeed.address, + 0, + false, + ); + await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); + + await depositInstantWithMorphoTest( + { + depositVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + morphoVaultMock: daiMorphoVault, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('toggle mid-flight: morpho enabled then disabled', async () => { + const { + owner, + depositVaultWithMorpho, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + ); + + await addPaymentTokenTest( + { vault: depositVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); + + // Deposit 1: morpho enabled + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + true, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 200); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMorpho, + 200, + ); + + await depositInstantWithMorphoTest( + { + depositVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + morphoVaultMock, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + + // Deposit 2: morpho disabled + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + false, + ); + + await depositInstantWithMorphoTest( + { + depositVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + morphoVaultMock, + expectedMorphoDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + }); - await rejectRequestTest( - { - depositVault: depositVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - request.requestId!, - ); + describe('depositRequest()', () => { + it('deposit request 100 USDC with morpho auto-invest', async () => { + const { + owner, + depositVaultWithMorpho, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + true, + ); + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + ); + await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMorpho, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + await depositRequestWithMorphoTest( + { + depositVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + morphoVaultMock, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit request with morpho auto-invest, deposit reverts, fallback enabled: fallback to normal flow', async () => { + const { + owner, + depositVaultWithMorpho, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + true, + ); + await setAutoInvestFallbackEnabledMorphoTest( + { depositVaultWithMorpho, owner }, + true, + ); + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + ); + await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMorpho, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + await morphoVaultMock.setShouldRevertDeposit(true); + + await depositRequestWithMorphoTest( + { + depositVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + morphoVaultMock, + expectedMorphoDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('should fail: deposit request with morpho auto-invest, deposit reverts, fallback disabled', async () => { + const { + owner, + depositVaultWithMorpho, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoDepositsEnabledTest( + { depositVaultWithMorpho, owner }, + true, + ); + await setMorphoVaultTest( + { depositVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + ); + await setMinAmountTest({ vault: depositVaultWithMorpho, owner }, 10); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithMorpho, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + await morphoVaultMock.setShouldRevertDeposit(true); + + await depositRequestWithMorphoTest( + { + depositVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + morphoVaultMock, + expectedMorphoDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'AutoInvestFailed', + }, + }, + ); + }); + }); }); - }); -}); + }, +); diff --git a/test/unit/DepositVaultWithUSTB.test.ts b/test/unit/DepositVaultWithUSTB.test.ts index d3b1d001..fcbcbfd8 100644 --- a/test/unit/DepositVaultWithUSTB.test.ts +++ b/test/unit/DepositVaultWithUSTB.test.ts @@ -1,6157 +1,349 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; import { constants } from 'ethers'; -import { parseUnits } from 'ethers/lib/utils'; -import { ethers } from 'hardhat'; -import { encodeFnSelector } from '../../helpers/utils'; import { - DepositVaultTest__factory, - EUsdDepositVault__factory, - ManageableVaultTester__factory, - MBasisDepositVault__factory, + baseInitParamsDv, + depositVaultSuits, +} from './suits/deposit-vault.suits'; + +import { + DepositVaultWithUSTB__factory, + DepositVaultWithUSTBTest__factory, } from '../../typechain-types'; -import { acErrors, blackList, greenList } from '../common/ac.helpers'; +import { acErrors } from '../common/ac.helpers'; import { approveBase18, + InitializeParamCase, mintToken, - pauseVault, - pauseVaultFn, + validateImplementation, } from '../common/common.helpers'; -import { setRoundData } from '../common/data-feed.helpers'; import { - depositInstantWithUstbTest, setMockUstbStablecoinConfig, setUstbDepositsEnabledTest, + depositInstantWithUstbTest, } from '../common/deposit-vault-ustb.helpers'; -import { - approveRequestTest, - depositInstantTest, - depositRequestTest, - rejectRequestTest, - safeApproveRequestTest, - safeBulkApproveRequestTest, -} from '../common/deposit-vault.helpers'; import { defaultDeploy } from '../common/fixtures'; -import { greenListEnable } from '../common/greenlist.helpers'; import { addPaymentTokenTest, - addWaivedFeeAccountTest, - changeTokenAllowanceTest, - removePaymentTokenTest, - removeWaivedFeeAccountTest, - setInstantFeeTest, - setInstantDailyLimitTest, setMinAmountTest, - setMinAmountToDepositTest, - setVariabilityToleranceTest, - withdrawTest, - changeTokenFeeTest, } from '../common/manageable-vault.helpers'; -import { sanctionUser } from '../common/with-sanctions-list.helpers'; - -describe('DepositVaultWithUSTB', function () { - it('deployment', async () => { - const { - depositVaultWithUSTB, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - roles, - ustbToken, - } = await loadFixture(defaultDeploy); - - expect(await depositVaultWithUSTB.mToken()).eq(mTBILL.address); - - expect(await depositVaultWithUSTB.paused()).eq(false); - - expect(await depositVaultWithUSTB.tokensReceiver()).eq( - tokensReceiver.address, - ); - expect(await depositVaultWithUSTB.feeReceiver()).eq(feeReceiver.address); - - expect(await depositVaultWithUSTB.ONE_HUNDRED_PERCENT()).eq('10000'); - - expect(await depositVaultWithUSTB.minMTokenAmountForFirstDeposit()).eq('0'); - expect(await depositVaultWithUSTB.minAmount()).eq(parseUnits('100')); - - expect(await depositVaultWithUSTB.instantFee()).eq('100'); - - expect(await depositVaultWithUSTB.instantDailyLimit()).eq( - parseUnits('100000'), - ); - - expect(await depositVaultWithUSTB.mTokenDataFeed()).eq( - mTokenToUsdDataFeed.address, - ); - expect(await depositVaultWithUSTB.variationTolerance()).eq(1); - - expect(await depositVaultWithUSTB.vaultRole()).eq( - roles.tokenRoles.mTBILL.depositVaultAdmin, - ); +import { + initializeDvWithUstb, + InitializerParamsDvWithUstb, +} from '../common/vault-initializer.helpers'; + +const baseInitParamsDvWithUstb = ( + fixture: Parameters[0], +): InitializerParamsDvWithUstb => ({ + ...baseInitParamsDv(fixture), + ustbToken: fixture.ustbToken, +}); - expect(await depositVaultWithUSTB.MANUAL_FULLFILMENT_TOKEN()).eq( - ethers.constants.AddressZero, - ); +const dvWithUstbInitializeParamCases: InitializeParamCase[] = + [ + { + title: 'ustbToken is zero address', + params: { ustbToken: constants.AddressZero }, + revertCustomError: { + customErrorName: 'InvalidAddress', + args: [constants.AddressZero], + }, + }, + ]; + +depositVaultSuits( + 'DepositVaultWithUSTB', + defaultDeploy, + { + createNew: async (owner: SignerWithAddress) => + new DepositVaultWithUSTBTest__factory(owner).deploy(), + key: 'depositVaultWithUSTB', + }, + async (fixture) => { + const { depositVaultWithUSTB, ustbToken } = fixture; expect(await depositVaultWithUSTB.ustb()).eq(ustbToken.address); - }); - - it('failing deployment', async () => { - const { - accessControl, - mTBILL, - owner, - mTokenToUsdDataFeed, - feeReceiver, - tokensReceiver, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - const depositVaultWithUSTB = await new DepositVaultTest__factory( - owner, - ).deploy(); - - await expect( - depositVaultWithUSTB.initialize( - ethers.constants.AddressZero, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - parseUnits('100'), - constants.MaxUint256, - ), - ).to.be.reverted; - await expect( - depositVaultWithUSTB.initialize( - accessControl.address, - { - mToken: ethers.constants.AddressZero, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - parseUnits('100'), - constants.MaxUint256, - ), - ).to.be.reverted; - await expect( - depositVaultWithUSTB.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: ethers.constants.AddressZero, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - parseUnits('100'), - constants.MaxUint256, - ), - ).to.be.reverted; - await expect( - depositVaultWithUSTB.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: ethers.constants.AddressZero, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - parseUnits('100'), - constants.MaxUint256, - ), - ).to.be.reverted; - await expect( - depositVaultWithUSTB.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: ethers.constants.AddressZero, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - parseUnits('100'), - constants.MaxUint256, - ), - ).to.be.reverted; - await expect( - depositVaultWithUSTB.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100001, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - parseUnits('100'), - constants.MaxUint256, - ), - ).to.be.reverted; - }); - - it('MBasisDepositVault', async () => { - const fixture = await loadFixture(defaultDeploy); - - const tester = await new MBasisDepositVault__factory( - fixture.owner, - ).deploy(); - - expect(await tester.vaultRole()).eq( - await tester.M_BASIS_DEPOSIT_VAULT_ADMIN_ROLE(), - ); - }); - - it('EUsdDepositVault', async () => { - const fixture = await loadFixture(defaultDeploy); - - const tester = await new EUsdDepositVault__factory(fixture.owner).deploy(); - - expect(await tester.vaultRole()).eq( - await tester.E_USD_DEPOSIT_VAULT_ADMIN_ROLE(), - ); - }); - - describe('initialization', () => { - it('should fail: cal; initialize() when already initialized', async () => { - const { depositVaultWithUSTB } = await loadFixture(defaultDeploy); - - await expect( - depositVaultWithUSTB[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,uint256,uint256,address)' - ]( - constants.AddressZero, - { - mToken: constants.AddressZero, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: constants.AddressZero, - tokensReceiver: constants.AddressZero, - }, - { - instantFee: 0, - instantDailyLimit: 0, - }, - constants.AddressZero, - 0, - 0, - 0, - 0, - constants.AddressZero, - ), - ).revertedWith('Initializable: contract is already initialized'); - }); - - it('should fail: call with initializing == false', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initializeWithoutInitializer( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - ), - ).revertedWith('Initializable: contract is not initializing'); - }); - - it('should fail: when _tokensReceiver == address(this)', async () => { - const { - owner, - accessControl, - mTBILL, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: vault.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - ), - ).revertedWith('invalid address'); - }); - it('should fail: when _feeReceiver == address(this)', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: vault.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - ), - ).revertedWith('invalid address'); - }); - it('should fail: when limit = 0', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: 0, - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - ), - ).revertedWith('zero limit'); - }); - it('should fail: when mToken dataFeed address zero', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - ), - ).revertedWith('zero address'); - }); - it('should fail: when variationTolarance zero', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mockedSanctionsList, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 0, - parseUnits('100'), - ), - ).revertedWith('fee == 0'); - }); - }); - - describe('setMinMTokenAmountForFirstDeposit()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, depositVaultWithUSTB, regularAccounts } = - await loadFixture(defaultDeploy); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithUSTB, owner }, - 1.1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, depositVaultWithUSTB } = await loadFixture(defaultDeploy); - await setMinAmountToDepositTest( - { depositVault: depositVaultWithUSTB, owner }, - 1.1, - ); - }); - }); - - describe('setMinAmount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, depositVaultWithUSTB, regularAccounts } = - await loadFixture(defaultDeploy); - - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 1.1, { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, + await validateImplementation(DepositVaultWithUSTB__factory); + }, + { + deployUninitialized: (fixture) => + new DepositVaultWithUSTBTest__factory(fixture.owner).deploy(), + initialize: async (fixture, params, opt) => { + await initializeDvWithUstb( + { ...baseInitParamsDvWithUstb(fixture), ...params }, + opt?.contract, + opt, + ); + }, + extraParamCases: dvWithUstbInitializeParamCases, + }, + async (defaultDeploy) => { + describe('DepositVaultWithUSTB', function () { + describe('depositInstant()', async () => { + it('should fail: when ustbDepositsEnabled is true and payment token is not set in USTB contract', async () => { + const { + owner, + depositVaultWithUSTB, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + ustbToken, + } = await loadFixture(defaultDeploy); + + await setUstbDepositsEnabledTest( + { + depositVaultWithUSTB, + owner, + }, + true, + ); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithUSTB, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithUSTB, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); + + await depositInstantWithUstbTest( + { + depositVaultWithUSTB, + owner, + mTBILL, + mTokenToUsdDataFeed, + ustbToken, + expectedUstbDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'UnsupportedUSTBToken', + }, + }, + ); + }); + + it('should fail: when ustbDepositsEnabled is true and payment token is set in USTB contract but fee is not 0', async () => { + const { + owner, + depositVaultWithUSTB, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + ustbToken, + } = await loadFixture(defaultDeploy); + + await setUstbDepositsEnabledTest( + { + depositVaultWithUSTB, + owner, + }, + true, + ); + + await setMockUstbStablecoinConfig({ ustbToken }, stableCoins.usdc, { + fee: 100, + sweepDestination: ustbToken.address, + }); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithUSTB, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithUSTB, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); + + await depositInstantWithUstbTest( + { + depositVaultWithUSTB, + owner, + mTBILL, + mTokenToUsdDataFeed, + ustbToken, + expectedUstbDeposited: false, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'USTBFeeNotZero', + }, + }, + ); + }); + + it('deposit 100 USDC when ustbDepositsEnabled is true', async () => { + const { + owner, + depositVaultWithUSTB, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + ustbToken, + } = await loadFixture(defaultDeploy); + + await setUstbDepositsEnabledTest( + { + depositVaultWithUSTB, + owner, + }, + true, + ); + await setMockUstbStablecoinConfig({ ustbToken }, stableCoins.usdc); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithUSTB, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithUSTB, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); + + await depositInstantWithUstbTest( + { + depositVaultWithUSTB, + owner, + mTBILL, + mTokenToUsdDataFeed, + ustbToken, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when ustbDepositsEnabled is false and payment token is not set in USTB contract', async () => { + const { + owner, + depositVaultWithUSTB, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + ustbToken, + } = await loadFixture(defaultDeploy); + + await mintToken(stableCoins.usdc, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.usdc, + depositVaultWithUSTB, + 100, + ); + await addPaymentTokenTest( + { vault: depositVaultWithUSTB, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); + + await depositInstantWithUstbTest( + { + depositVaultWithUSTB, + owner, + mTBILL, + mTokenToUsdDataFeed, + ustbToken, + }, + stableCoins.usdc, + 100, + { + from: regularAccounts[0], + }, + ); + }); }); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, depositVaultWithUSTB } = await loadFixture(defaultDeploy); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 1.1); - }); - }); - - describe('setInstantDailyLimit()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, depositVaultWithUSTB, regularAccounts } = - await loadFixture(defaultDeploy); - - await setInstantDailyLimitTest( - { vault: depositVaultWithUSTB, owner }, - parseUnits('1000'), - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('should fail: try to set 0 limit', async () => { - const { owner, depositVaultWithUSTB } = await loadFixture(defaultDeploy); - - await setInstantDailyLimitTest( - { vault: depositVaultWithUSTB, owner }, - constants.Zero, - { - revertMessage: 'MV: limit zero', - }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, depositVaultWithUSTB } = await loadFixture(defaultDeploy); - await setInstantDailyLimitTest( - { vault: depositVaultWithUSTB, owner }, - parseUnits('1000'), - ); - }); - }); - - describe('addPaymentToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - ethers.constants.AddressZero, - ethers.constants.AddressZero, - 0, - false, - constants.MaxUint256, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when token is already added', async () => { - const { depositVaultWithUSTB, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - constants.MaxUint256, - { - revertMessage: 'MV: already added', - }, - ); - }); - - it('should fail: when token dataFeed address zero', async () => { - const { depositVaultWithUSTB, stableCoins, owner } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - constants.AddressZero, - 0, - false, - constants.MaxUint256, - { - revertMessage: 'zero address', - }, - ); - }); - - it('call when allowance is zero', async () => { - const { depositVaultWithUSTB, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - constants.Zero, - ); - }); - - it('call when allowance is not uint256 max', async () => { - const { depositVaultWithUSTB, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - parseUnits('100'), - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role and add 3 options on a row', async () => { - const { depositVaultWithUSTB, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - }); - }); - - describe('addWaivedFeeAccount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVaultWithUSTB, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if account fee already waived', async () => { - const { depositVaultWithUSTB, owner } = await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVaultWithUSTB, owner }, - owner.address, - ); - await addWaivedFeeAccountTest( - { vault: depositVaultWithUSTB, owner }, - owner.address, - { revertMessage: 'MV: already added' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, owner } = await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVaultWithUSTB, owner }, - owner.address, - ); - }); - }); - - describe('removeWaivedFeeAccount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await removeWaivedFeeAccountTest( - { vault: depositVaultWithUSTB, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if account not found in restriction', async () => { - const { depositVaultWithUSTB, owner } = await loadFixture(defaultDeploy); - await removeWaivedFeeAccountTest( - { vault: depositVaultWithUSTB, owner }, - owner.address, - { revertMessage: 'MV: not found' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, owner } = await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: depositVaultWithUSTB, owner }, - owner.address, - ); - await removeWaivedFeeAccountTest( - { vault: depositVaultWithUSTB, owner }, - owner.address, - ); - }); - }); - - describe('setFee()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setInstantFeeTest( - { vault: depositVaultWithUSTB, owner }, - ethers.constants.Zero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if new value greater then 100%', async () => { - const { depositVaultWithUSTB, owner } = await loadFixture(defaultDeploy); - await setInstantFeeTest({ vault: depositVaultWithUSTB, owner }, 10001, { - revertMessage: 'fee > 100%', + describe('setUstbDepositsEnabled', () => { + it('should fail: call from address without vault admin role', async () => { + const { depositVaultWithUSTB, owner, regularAccounts } = + await loadFixture(defaultDeploy); + await setUstbDepositsEnabledTest( + { depositVaultWithUSTB, owner }, + true, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('call from address with vault admin role', async () => { + const { depositVaultWithUSTB, owner } = await loadFixture( + defaultDeploy, + ); + await setUstbDepositsEnabledTest( + { depositVaultWithUSTB, owner }, + true, + ); + }); + + it('set true when ustbDepositsEnabled is already true', async () => { + const { depositVaultWithUSTB, owner } = await loadFixture( + defaultDeploy, + ); + await setUstbDepositsEnabledTest( + { depositVaultWithUSTB, owner }, + true, + ); + await setUstbDepositsEnabledTest( + { depositVaultWithUSTB, owner }, + true, + ); + }); + + it('set false when ustbDepositsEnabled is already false', async () => { + const { depositVaultWithUSTB, owner } = await loadFixture( + defaultDeploy, + ); + await setUstbDepositsEnabledTest( + { depositVaultWithUSTB, owner }, + false, + ); + }); }); }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, owner } = await loadFixture(defaultDeploy); - await setInstantFeeTest({ vault: depositVaultWithUSTB, owner }, 100); - }); - }); - - describe('setVariabilityTolerance()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setVariabilityToleranceTest( - { vault: depositVaultWithUSTB, owner }, - ethers.constants.Zero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if new value zero', async () => { - const { depositVaultWithUSTB, owner } = await loadFixture(defaultDeploy); - await setVariabilityToleranceTest( - { vault: depositVaultWithUSTB, owner }, - ethers.constants.Zero, - { revertMessage: 'fee == 0' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, owner } = await loadFixture(defaultDeploy); - await setVariabilityToleranceTest( - { vault: depositVaultWithUSTB, owner }, - 100, - ); - }); - }); - - describe('removePaymentToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await removePaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when token is not exists', async () => { - const { owner, depositVaultWithUSTB, stableCoins } = await loadFixture( - defaultDeploy, - ); - await removePaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai.address, - { revertMessage: 'MV: not exists' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await removePaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai.address, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role and add 3 options on a row', async () => { - const { depositVaultWithUSTB, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - - await removePaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai.address, - ); - await removePaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdc.address, - ); - await removePaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdt.address, - ); - - await removePaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdt.address, - { revertMessage: 'MV: not exists' }, - ); - }); - }); - - describe('withdrawToken()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await withdrawTest( - { vault: depositVaultWithUSTB, owner }, - ethers.constants.AddressZero, - 0, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when there is no token in vault', async () => { - const { owner, depositVaultWithUSTB, regularAccounts, stableCoins } = - await loadFixture(defaultDeploy); - await withdrawTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - 1, - regularAccounts[0], - { revertMessage: 'ERC20: transfer amount exceeds balance' }, - ); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, regularAccounts, stableCoins, owner } = - await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, depositVaultWithUSTB, 1); - await withdrawTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - 1, - regularAccounts[0], - ); - }); - }); - - describe('freeFromMinAmount()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { depositVaultWithUSTB, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVaultWithUSTB - .connect(regularAccounts[0]) - .freeFromMinAmount(regularAccounts[1].address, true), - ).to.be.revertedWith('WMAC: hasnt role'); - }); - it('should not fail', async () => { - const { depositVaultWithUSTB, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVaultWithUSTB.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.not.reverted; - - expect( - await depositVaultWithUSTB.isFreeFromMinAmount( - regularAccounts[0].address, - ), - ).to.eq(true); - }); - it('should fail: already in list', async () => { - const { depositVaultWithUSTB, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - depositVaultWithUSTB.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.not.reverted; - - expect( - await depositVaultWithUSTB.isFreeFromMinAmount( - regularAccounts[0].address, - ), - ).to.eq(true); - - await expect( - depositVaultWithUSTB.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.revertedWith('DV: already free'); - }); - }); - - describe('changeTokenAllowance()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await changeTokenAllowanceTest( - { vault: depositVaultWithUSTB, owner }, - ethers.constants.AddressZero, - 0, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: token not exist', async () => { - const { depositVaultWithUSTB, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - await changeTokenAllowanceTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: token not exists' }, - ); - }); - it('should fail: allowance zero', async () => { - const { depositVaultWithUSTB, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: zero allowance' }, - ); - }); - it('should fail: if mint exceed allowance', async () => { - const { - depositVaultWithUSTB, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100000); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100000); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai.address, - 100, - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai.address, - 100000000, - ); - }); - it('should decrease if allowance < UINT_MAX', async () => { - const { - depositVaultWithUSTB, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100000); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100000); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai.address, - parseUnits('1000'), - ); - - const tokenConfigBefore = await depositVaultWithUSTB.tokensConfig( - stableCoins.dai.address, - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 999, - ); - - const tokenConfigAfter = await depositVaultWithUSTB.tokensConfig( - stableCoins.dai.address, - ); - - expect(tokenConfigBefore.allowance.sub(tokenConfigAfter.allowance)).eq( - parseUnits('999'), - ); - }); - it('should not decrease if allowance = UINT_MAX', async () => { - const { - depositVaultWithUSTB, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100000); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100000); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai.address, - constants.MaxUint256, - ); - - const tokenConfigBefore = await depositVaultWithUSTB.tokensConfig( - stableCoins.dai.address, - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 999, - ); - - const tokenConfigAfter = await depositVaultWithUSTB.tokensConfig( - stableCoins.dai.address, - ); - - expect(tokenConfigBefore.allowance).eq(tokenConfigAfter.allowance); - }); - }); - - describe('changeTokenFee()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await changeTokenFeeTest( - { vault: depositVaultWithUSTB, owner }, - ethers.constants.AddressZero, - 0, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: token not exist', async () => { - const { depositVaultWithUSTB, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - await changeTokenFeeTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: token not exists' }, - ); - }); - it('should fail: fee > 100%', async () => { - const { depositVaultWithUSTB, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai.address, - 10001, - { revertMessage: 'fee > 100%' }, - ); - }); - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { depositVaultWithUSTB, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai.address, - 100, - ); - }); - }); - - describe('depositInstant()', async () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to deposit 0 amount', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 0, - { - revertMessage: 'DV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'depositInstant(address,uint256,uint256,bytes32)', - ); - await pauseVaultFn(depositVaultWithUSTB, selector); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: when rounding is invalid', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100.0000000001, - { - revertMessage: 'MV: invalid rounding', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 10); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(stableCoins.dai, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minAmountToDepositTest', async () => { - const { - depositVaultWithUSTB, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 100_000); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithUSTB, - 100_000, - ); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithUSTB, owner }, - 100_000, - ); - await setInstantDailyLimitTest( - { vault: depositVaultWithUSTB, owner }, - 150_000, - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'DV: mint amount < min', - }, - ); - }); - - it('should fail: call for amount < minAmount', async () => { - const { - depositVaultWithUSTB, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 100_000); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithUSTB, - 100_000, - ); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithUSTB, owner }, - 100_000, - ); - await setInstantDailyLimitTest( - { vault: depositVaultWithUSTB, owner }, - 150_000, - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99, - { - revertMessage: 'DV: mToken amount < min', - }, - ); - }); - - it('should fail: if exceed allowance of deposit for token', async () => { - const { - depositVaultWithUSTB, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.dai, owner, 100_000); - await changeTokenAllowanceTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai.address, - 100, - ); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithUSTB, - 100_000, - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - - it('should fail: if mint limit exceeded', async () => { - const { - depositVaultWithUSTB, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.dai, owner, 100_000); - await setInstantDailyLimitTest( - { vault: depositVaultWithUSTB, owner }, - 1000, - ); - - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithUSTB, - 100_000, - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed limit', - }, - ); - }); - - it('should fail: if min receive amount greater then actual', async () => { - const { - depositVaultWithUSTB, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.dai, owner, 100_000); - - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithUSTB, - 100_000, - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - minAmount: parseUnits('100000'), - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'DV: minReceiveAmount > actual', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 10000, - true, - ); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'DV: mToken amount < min', - }, - ); - - await removePaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: depositVaultWithUSTB, owner }, 10000); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { revertMessage: 'DV: mToken amount < min' }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist ', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await depositVaultWithUSTB.setGreenlistEnable(true); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist ', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await depositVaultWithUSTB.setGreenlistEnable(true); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: when function paused (custom recipient overload)', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'depositInstant(address,uint256,uint256,bytes32,address)', - ); - await pauseVaultFn(depositVaultWithUSTB, selector); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: when ustbDepositsEnabled is true and payment token is not set in USTB contract', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - ustbToken, - } = await loadFixture(defaultDeploy); - - await setUstbDepositsEnabledTest( - { - depositVaultWithUSTB, - owner, - }, - true, - ); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositInstantWithUstbTest( - { - depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - ustbToken, - expectedUstbDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'DVU: unsupported USTB token', - }, - ); - }); - - it('should fail: when ustbDepositsEnabled is true and payment token is set in USTB contract but fee is not 0', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - ustbToken, - } = await loadFixture(defaultDeploy); - - await setUstbDepositsEnabledTest( - { - depositVaultWithUSTB, - owner, - }, - true, - ); - - await setMockUstbStablecoinConfig({ ustbToken }, stableCoins.usdc, { - fee: 100, - sweepDestination: ustbToken.address, - }); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositInstantWithUstbTest( - { - depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - ustbToken, - expectedUstbDeposited: false, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'DVU: USTB fee is not 0', - }, - ); - }); - - it('deposit 100 DAI, greenlist enabled and user in greenlist ', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await depositVaultWithUSTB.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 USDC when ustbDepositsEnabled is true', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - ustbToken, - } = await loadFixture(defaultDeploy); - - await setUstbDepositsEnabledTest( - { - depositVaultWithUSTB, - owner, - }, - true, - ); - await setMockUstbStablecoinConfig({ ustbToken }, stableCoins.usdc); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositInstantWithUstbTest( - { - depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - ustbToken, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('when ustbDepositsEnabled is false and payment token is not set in USTB contract', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - ustbToken, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositInstantWithUstbTest( - { - depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - ustbToken, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI, greenlist enabled and user in greenlist, tokenIn not stablecoin', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await depositVaultWithUSTB.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI, when price of stable is 1.03$ and mToken price is 5$', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('deposit 100 DAI, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('deposit 100 DAI, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await depositVaultWithUSTB.freeFromMinAmount(owner.address, true); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('deposit 100 DAI, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await addWaivedFeeAccountTest( - { vault: depositVaultWithUSTB, owner }, - owner.address, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - waivedFee: true, - }, - stableCoins.dai, - 100, - ); - }); - - it('deposit 100 DAI (custom recipient overload)', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI when recipient == msg.sender (custom recipient overload)', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[0], - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI when other overload of depositInstant is paused (custom recipient overload)', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - depositVaultWithUSTB, - encodeFnSelector('depositInstant(address,uint256,uint256,bytes32)'), - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI when other overload of depositInstant is paused', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - depositVaultWithUSTB, - encodeFnSelector( - 'depositInstant(address,uint256,uint256,bytes32,address)', - ), - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - }); - - describe('depositRequest()', async () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to deposit 0 amount', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 0, - { - revertMessage: 'DV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'depositRequest(address,uint256,bytes32)', - ); - await pauseVaultFn(depositVaultWithUSTB, selector); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: when function paused (custom recipient overload)', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'depositRequest(address,uint256,bytes32,address)', - ); - await pauseVaultFn(depositVaultWithUSTB, selector); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: when rounding is invalid', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100.0000000001, - { - revertMessage: 'MV: invalid rounding', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 10); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(stableCoins.dai, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minAmountToDepositTest', async () => { - const { - depositVaultWithUSTB, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 100_000); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithUSTB, - 100_000, - ); - - await setMinAmountToDepositTest( - { depositVault: depositVaultWithUSTB, owner }, - 100_000, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'DV: mint amount < min', - }, - ); - }); - - it('should fail: if exceed allowance of deposit for token', async () => { - const { - depositVaultWithUSTB, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(stableCoins.dai, owner, 100_000); - await changeTokenAllowanceTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai.address, - 100, - ); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithUSTB, - 100_000, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - - it('should fail: if token fee = 100%', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 10000, - true, - ); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'DV: mToken amount < min', - }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist ', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await depositVaultWithUSTB.setGreenlistEnable(true); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist ', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctionlist ', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - } = await loadFixture(defaultDeploy); - - await depositVaultWithUSTB.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctionlist (custom recipient overload)', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('deposit 100 DAI, greenlist enabled and user in greenlist ', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await greenListEnable( - { greenlistable: greenListableTester, owner }, - true, - ); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit request with 100 DAI, when price of stable is 1.03$ and mToken price is 5$', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('deposit request with 100 DAI, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('deposit request with 100 DAI, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await depositVaultWithUSTB.freeFromMinAmount(owner.address, true); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('deposit request with 100 DAI, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await addWaivedFeeAccountTest( - { vault: depositVaultWithUSTB, owner }, - owner.address, - ); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - waivedFee: true, - }, - stableCoins.dai, - 100, - ); - }); - - it('deposit 100 (custom recipient overload)', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 when recipient == msg.sender (custom recipient overload)', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[0], - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI when other overload of depositRequest is paused (custom recipient overload)', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - depositVaultWithUSTB, - encodeFnSelector('depositRequest(address,uint256,bytes32)'), - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('deposit 100 DAI when other overload of depositRequest is paused', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - depositVaultWithUSTB, - encodeFnSelector('depositRequest(address,uint256,bytes32,address)'), - ); - - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - }); - - describe('approveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - depositVaultWithUSTB, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await approveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('5'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await approveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('5'), - { - revertMessage: 'DV: request not exist', - }, - ); - }); - - it('should fail: request already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('5'), - ); - await approveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('5'), - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('5'), - ); - }); - }); - - describe('safeApproveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - depositVaultWithUSTB, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await safeApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'DV: request not exist', - }, - ); - }); - - it('should fail: if new rate greater then variabilityTolerance', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - await safeApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('6'), - { - revertMessage: 'MV: exceed price diviation', - }, - ); - }); - - it('should fail: if new rate lower then variabilityTolerance', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - await safeApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('4'), - { - revertMessage: 'MV: exceed price diviation', - }, - ); - }); - - it('should fail: request already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('5.000001'), - ); - await safeApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('5.000001'), - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('5.000000001'), - ); - }); - }); - - describe('safeBulkApproveRequest() (custom price overload)', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - depositVaultWithUSTB, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }], - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }], - parseUnits('1'), - { - revertMessage: 'DV: request not exist', - }, - ); - }); - - it('should fail: if new rate greater then variabilityTolerance', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - parseUnits('6'), - { - revertMessage: 'MV: exceed price diviation', - }, - ); - }); - - it('should fail: if new rate lower then variabilityTolerance', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - parseUnits('4'), - { - revertMessage: 'MV: exceed price diviation', - }, - ); - }); - - it('should fail: request already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - parseUnits('5.000001'), - ); - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - parseUnits('5.000001'), - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when one of them already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 200); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - - await safeApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - 0, - parseUnits('5.000001'), - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }, { id: 0 }], - parseUnits('5.000001'), - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when couple of them have equal id', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 200); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - - await safeApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - 0, - parseUnits('5.000001'), - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }, { id: 1 }], - parseUnits('5.000001'), - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('approve 1 request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - parseUnits('5.000000001'), - ); - }); - - it('approve 2 requests from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 200); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 0 }, { id: 1 }], - parseUnits('5.000000001'), - ); - }); - - it('approve 10 requests from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 1000); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 1000); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - for (let i = 0; i < 10; i++) { - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - } - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - Array.from({ length: 10 }, (_, i) => ({ id: i })), - parseUnits('5.000000001'), - ); - }); - }); - - describe('safeBulkApproveRequest() (current price overload)', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - depositVaultWithUSTB, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }], - undefined, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }], - undefined, - { - revertMessage: 'DV: request not exist', - }, - ); - }); - - it('should fail: if new rate greater then variabilityTolerance', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 10); - - const requestId = 0; - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - undefined, - { - revertMessage: 'MV: exceed price diviation', - }, - ); - }); - - it('should fail: if new rate lower then variabilityTolerance', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 3); - - const requestId = 0; - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - undefined, - { - revertMessage: 'MV: exceed price diviation', - }, - ); - }); - - it('should fail: request already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - undefined, - ); - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - undefined, - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when one of them already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 200); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 0 }], - undefined, - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }, { id: 0 }], - undefined, - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when couple of the have equal id', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 200); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 0 }], - undefined, - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }, { id: 1 }], - undefined, - { revertMessage: 'DV: request not pending' }, - ); - }); - - it('approve 1 request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: requestId }], - undefined, - ); - }); - - it('approve 2 requests from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 200); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 200); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 0 }, { id: 1 }], - undefined, - ); - }); - - it('approve 10 requests from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 1000); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 1000); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - for (let i = 0; i < 10; i++) { - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - } - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - Array.from({ length: 10 }, (_, i) => ({ id: i })), - undefined, - ); - }); - - it('approve 10 requests from vaut admin account when different users are recievers', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 1000); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 1000); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - for (let i = 0; i < 10; i++) { - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[i], - }, - stableCoins.dai, - 100, - ); - } - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - Array.from({ length: 10 }, (_, i) => ({ id: i })), - undefined, - ); - }); - - it('approve 2 requests from vaut admin account when each request has different token', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await mintToken(stableCoins.usdc, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - await safeBulkApproveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 0 }, { id: 1 }], - undefined, - ); - }); - }); - - describe('rejectRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - depositVaultWithUSTB, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await rejectRequestTest( - { - depositVault: depositVaultWithUSTB, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await rejectRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - { - revertMessage: 'DV: request not exist', - }, - ); - }); - - it('should fail: request is already rejected', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - ); - - await rejectRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - { - revertMessage: 'DV: request not pending', - }, - ); - }); - - it('reject request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - ); - }); - }); - - describe('setUstbDepositsEnabled', () => { - it('should fail: call from address without vault admin role', async () => { - const { depositVaultWithUSTB, owner, regularAccounts } = - await loadFixture(defaultDeploy); - await setUstbDepositsEnabledTest({ depositVaultWithUSTB, owner }, true, { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }); - }); - - it('call from address with vault admin role', async () => { - const { depositVaultWithUSTB, owner } = await loadFixture(defaultDeploy); - await setUstbDepositsEnabledTest({ depositVaultWithUSTB, owner }, true); - }); - - it('set true when ustbDepositsEnabled is already true', async () => { - const { depositVaultWithUSTB, owner } = await loadFixture(defaultDeploy); - await setUstbDepositsEnabledTest({ depositVaultWithUSTB, owner }, true); - await setUstbDepositsEnabledTest({ depositVaultWithUSTB, owner }, true); - }); - - it('set false when ustbDepositsEnabled is already false', async () => { - const { depositVaultWithUSTB, owner } = await loadFixture(defaultDeploy); - await setUstbDepositsEnabledTest({ depositVaultWithUSTB, owner }, false); - }); - }); - - describe('depositInstant() complex', () => { - it('should fail: when is paused', async () => { - const { - depositVaultWithUSTB, - owner, - mTBILL, - stableCoins, - regularAccounts, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(depositVaultWithUSTB); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('is on pause, but admin can use everything', async () => { - const { - depositVaultWithUSTB, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(depositVaultWithUSTB); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('call for amount == minAmountToDepositTest', async () => { - const { - depositVaultWithUSTB, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 102_000); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithUSTB, - 102_000, - ); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountToDepositTest( - { depositVault: depositVaultWithUSTB, owner }, - 100_000, - ); - await setInstantDailyLimitTest( - { vault: depositVaultWithUSTB, owner }, - parseUnits('150000'), - ); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 102_000, - ); - }); - - it('call for amount == minAmountToDepositTest+1, then deposit with amount 100', async () => { - const { - depositVaultWithUSTB, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 103_101); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithUSTB, - 103_101, - ); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountToDepositTest( - { depositVault: depositVaultWithUSTB, owner }, - 100_000, - ); - await setInstantDailyLimitTest( - { vault: depositVaultWithUSTB, owner }, - parseUnits('150000'), - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 103_001, - ); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('deposit 100 DAI, when price is 5$, 25 USDC when price is 5.1$, 14 USDT when price is 5.4$', async () => { - const { - owner, - mockedAggregator, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await mintToken(stableCoins.usdc, owner, 125); - await mintToken(stableCoins.usdt, owner, 114); - - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithUSTB, 125); - await approveBase18(owner, stableCoins.usdt, depositVaultWithUSTB, 114); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.04); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - - await setRoundData({ mockedAggregator }, 1); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 125, - ); - - await setRoundData({ mockedAggregator }, 1.01); - await depositInstantTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdt, - 114, - ); - }); - }); - - describe('depositRequest() complex', () => { - it('should fail: when is paused', async () => { - const { - depositVaultWithUSTB, - owner, - mTBILL, - stableCoins, - regularAccounts, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(depositVaultWithUSTB); - await mintToken(stableCoins.dai, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - depositVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('is on pause, but admin can use everything', async () => { - const { - depositVaultWithUSTB, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(depositVaultWithUSTB); - - await mintToken(stableCoins.dai, owner, 100); - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('call for amount == minAmountToDepositTest', async () => { - const { - depositVaultWithUSTB, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 105_000); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithUSTB, - 105_000, - ); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountToDepositTest( - { depositVault: depositVaultWithUSTB, owner }, - 100_000, - ); - await setInstantDailyLimitTest( - { vault: depositVaultWithUSTB, owner }, - 150_000, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 102_000, - ); - const requestId = 0; - - await approveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('5'), - ); - }); - - it('call for amount == minAmountToDepositTest+1, then deposit with amount 1', async () => { - const { - depositVaultWithUSTB, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(stableCoins.dai, owner, 105_101); - await approveBase18( - owner, - stableCoins.dai, - depositVaultWithUSTB, - 105_101, - ); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountToDepositTest( - { depositVault: depositVaultWithUSTB, owner }, - 100_000, - ); - await setInstantDailyLimitTest( - { vault: depositVaultWithUSTB, owner }, - 150_000, - ); - - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 102_001, - ); - let requestId = 0; - - await approveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('5'), - ); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - requestId = 1; - - await approveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('5'), - ); - }); - - it('deposit 100 DAI, when price is 5$, 25 USDC when price is 5.1$, 14 USDT when price is 5.4$', async () => { - const { - owner, - mockedAggregator, - depositVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, owner, 100); - await mintToken(stableCoins.usdc, owner, 125); - await mintToken(stableCoins.usdt, owner, 114); - - await approveBase18(owner, stableCoins.dai, depositVaultWithUSTB, 100); - await approveBase18(owner, stableCoins.usdc, depositVaultWithUSTB, 125); - await approveBase18(owner, stableCoins.usdt, depositVaultWithUSTB, 114); - - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 10); - - await setRoundData({ mockedAggregator }, 1.04); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - let requestId = 0; - - await approveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('5'), - ); - - await setRoundData({ mockedAggregator }, 1); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 125, - ); - requestId = 1; - - await approveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('5'), - ); - - await setRoundData({ mockedAggregator }, 1.01); - await depositRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdt, - 114, - ); - requestId = 2; - - await approveRequestTest( - { - depositVault: depositVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - requestId, - parseUnits('5'), - ); - }); - }); - - describe('ManageableVault internal functions', () => { - it('should fail: invalid rounding tokenTransferFromToTester()', async () => { - const { depositVaultWithUSTB, stableCoins, owner } = await loadFixture( - defaultDeploy, - ); - - await mintToken(stableCoins.usdc, owner, 1000); - - await approveBase18(owner, stableCoins.usdc, depositVaultWithUSTB, 1000); - - await expect( - depositVaultWithUSTB.tokenTransferFromToTester( - stableCoins.usdc.address, - owner.address, - depositVaultWithUSTB.address, - parseUnits('999.999999999'), - 8, - ), - ).revertedWith('MV: invalid rounding'); - }); - - it('should fail: invalid rounding tokenTransferToUserTester()', async () => { - const { depositVaultWithUSTB, stableCoins, owner } = await loadFixture( - defaultDeploy, - ); - - await mintToken(stableCoins.usdc, depositVaultWithUSTB, 1000); - - await expect( - depositVaultWithUSTB.tokenTransferToUserTester( - stableCoins.usdc.address, - owner.address, - parseUnits('999.999999999'), - 8, - ), - ).revertedWith('MV: invalid rounding'); - }); - }); - - describe('_convertUsdToToken', () => { - it('should fail: when amountUsd == 0', async () => { - const { depositVaultWithUSTB } = await loadFixture(defaultDeploy); - - await expect( - depositVaultWithUSTB.convertTokenToUsdTest(constants.AddressZero, 0), - ).revertedWith('DV: amount zero'); - }); - - it('should fail: when tokenRate == 0', async () => { - const { depositVaultWithUSTB } = await loadFixture(defaultDeploy); - - await depositVaultWithUSTB.setOverrideGetTokenRate(true); - await depositVaultWithUSTB.setGetTokenRateValue(0); - - await expect( - depositVaultWithUSTB.convertTokenToUsdTest(constants.AddressZero, 1), - ).revertedWith('DV: rate zero'); - }); - }); - - describe('_convertUsdToMToken', () => { - it('should fail: when rate == 0', async () => { - const { depositVaultWithUSTB } = await loadFixture(defaultDeploy); - - await depositVaultWithUSTB.setOverrideGetTokenRate(true); - await depositVaultWithUSTB.setGetTokenRateValue(0); - - await expect(depositVaultWithUSTB.convertUsdToMTokenTest(1)).revertedWith( - 'DV: rate zero', - ); - }); - }); - - describe('_calcAndValidateDeposit', () => { - it('should fail: when tokenOut is not MANUAL_FULLFILMENT_TOKEN but isFiat = true', async () => { - const { depositVaultWithUSTB, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: depositVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - parseUnits('100', 2), - true, - ); - - await setMinAmountTest({ vault: depositVaultWithUSTB, owner }, 0); - - await expect( - depositVaultWithUSTB.calcAndValidateDeposit( - constants.AddressZero, - stableCoins.dai.address, - parseUnits('100'), - true, - ), - ).revertedWith('DV: invalid mint amount'); - }); - }); -}); + }, +); diff --git a/test/unit/Greenlistable.test.ts b/test/unit/Greenlistable.test.ts index 126e54ae..667b5ab4 100644 --- a/test/unit/Greenlistable.test.ts +++ b/test/unit/Greenlistable.test.ts @@ -1,11 +1,12 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; -import { GreenlistableTester__factory } from '../../typechain-types'; +import { encodeFnSelector } from '../../helpers/utils'; import { acErrors, greenList, - greenListToggler, + setPermissionRoleTester, + setupGrantOperatorRole, unGreenList, } from '../common/ac.helpers'; import { defaultDeploy } from '../common/fixtures'; @@ -25,30 +26,6 @@ describe('Greenlistable', function () { ).eq(true); }); - it('onlyInitializing', async () => { - const { owner, accessControl } = await loadFixture(defaultDeploy); - - const greenListable = await new GreenlistableTester__factory( - owner, - ).deploy(); - - await expect( - greenListable.initializeWithoutInitializer(accessControl.address), - ).revertedWith('Initializable: contract is not initializing'); - }); - - it('onlyInitializing unchained', async () => { - const { owner, accessControl } = await loadFixture(defaultDeploy); - - const greenListable = await new GreenlistableTester__factory( - owner, - ).deploy(); - - await expect( - greenListable.initializeUnchainedWithoutInitializer(), - ).revertedWith('Initializable: contract is not initializing'); - }); - describe('modifier onlyGreenlisted', () => { it('should fail: call from greenlisted user', async () => { const { greenListableTester, regularAccounts, owner } = await loadFixture( @@ -62,7 +39,7 @@ describe('Greenlistable', function () { await expect( greenListableTester.onlyGreenlistedTester(regularAccounts[0].address), - ).revertedWith(acErrors.WMAC_HASNT_ROLE); + ).revertedWithCustomError(greenListableTester, 'NotGreenlisted'); }); it('call from not greenlisted user', async () => { @@ -70,7 +47,12 @@ describe('Greenlistable', function () { await loadFixture(defaultDeploy); await greenList( - { greenlistable: greenListableTester, accessControl, owner }, + { + greenlistable: greenListableTester, + accessControl, + owner, + role: await greenListableTester.greenlistAdminRole(), + }, regularAccounts[0], ); await expect( @@ -79,35 +61,6 @@ describe('Greenlistable', function () { }); }); - describe('modifier onlyGreenlistToggler', () => { - it('should fail: call from not greenlistToggler user', async () => { - const { greenListableTester, regularAccounts } = await loadFixture( - defaultDeploy, - ); - - await expect( - greenListableTester.onlyGreenlistTogglerTester( - regularAccounts[0].address, - ), - ).revertedWith(acErrors.WMAC_HASNT_ROLE); - }); - - it('call from greenlistToggler user', async () => { - const { accessControl, greenListableTester, owner, regularAccounts } = - await loadFixture(defaultDeploy); - - await greenListToggler( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - await expect( - greenListableTester.onlyGreenlistTogglerTester( - regularAccounts[0].address, - ), - ).not.reverted; - }); - }); - describe('setGreenlistEnable()', () => { it('should fail: call from user without GREENLIST_TOGGLER_ROLE role', async () => { const { greenListableTester, owner, regularAccounts } = await loadFixture( @@ -119,7 +72,7 @@ describe('Greenlistable', function () { true, { from: regularAccounts[0], - revertMessage: `WMAC: hasnt role`, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), }, ); }); @@ -131,7 +84,9 @@ describe('Greenlistable', function () { { greenlistable: greenListableTester, owner }, false, { - revertMessage: `GL: same enable status`, + revertCustomError: { + customErrorName: 'SameBoolValue', + }, }, ); }); @@ -143,6 +98,95 @@ describe('Greenlistable', function () { true, ); }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, greenListableTester, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + const greenlistAdmin = await greenListableTester.greenlistAdminRole(); + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: greenlistAdmin, + targetContract: greenListableTester.address, + functionSelector: selector, + grantOperator: owner, + }); + + const user = regularAccounts[0]; + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + greenListableTester.address, + selector, + [ + { + account: user.address, + enabled: true, + }, + ], + ); + + expect(await accessControl.hasRole(greenlistAdmin, user.address)).eq( + false, + ); + + await greenListEnable( + { greenlistable: greenListableTester, owner }, + true, + { from: user }, + ); + }); + + it('succeeds with scoped permission and greenlist admin role', async () => { + const { accessControl, greenListableTester, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + const greenlistAdmin = await greenListableTester.greenlistAdminRole(); + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + const user = regularAccounts[0]; + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: greenlistAdmin, + targetContract: greenListableTester.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + greenListableTester.address, + selector, + [ + { + account: user.address, + enabled: true, + }, + ], + ); + + await accessControl['grantRole(bytes32,address)']( + greenlistAdmin, + user.address, + ); + + expect(await accessControl.hasRole(greenlistAdmin, user.address)).eq( + true, + ); + + await greenListEnable( + { greenlistable: greenListableTester, owner }, + true, + { + from: user, + }, + ); + }); }); describe('addToGreenList', () => { @@ -151,11 +195,16 @@ describe('Greenlistable', function () { await loadFixture(defaultDeploy); await greenList( - { greenlistable: greenListableTester, accessControl, owner }, + { + greenlistable: greenListableTester, + accessControl, + owner, + role: await greenListableTester.greenlistedRole(), + }, regularAccounts[0], { from: regularAccounts[0], - revertMessage: `AccessControl: account ${regularAccounts[0].address.toLowerCase()} is missing role ${await accessControl.GREENLIST_OPERATOR_ROLE()}`, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), }, ); }); @@ -164,7 +213,12 @@ describe('Greenlistable', function () { const { accessControl, greenListableTester, owner, regularAccounts } = await loadFixture(defaultDeploy); await greenList( - { greenlistable: greenListableTester, accessControl, owner }, + { + greenlistable: greenListableTester, + accessControl, + owner, + role: await greenListableTester.greenlistedRole(), + }, regularAccounts[0], ); }); @@ -176,11 +230,16 @@ describe('Greenlistable', function () { await loadFixture(defaultDeploy); await unGreenList( - { greenlistable: greenListableTester, accessControl, owner }, + { + greenlistable: greenListableTester, + accessControl, + owner, + role: await greenListableTester.greenlistedRole(), + }, regularAccounts[0], { from: regularAccounts[0], - revertMessage: `AccessControl: account ${regularAccounts[0].address.toLowerCase()} is missing role ${await accessControl.GREENLIST_OPERATOR_ROLE()}`, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), }, ); }); @@ -189,7 +248,12 @@ describe('Greenlistable', function () { const { accessControl, greenListableTester, owner, regularAccounts } = await loadFixture(defaultDeploy); await unGreenList( - { greenlistable: greenListableTester, accessControl, owner }, + { + greenlistable: greenListableTester, + accessControl, + owner, + role: await greenListableTester.greenlistAdminRole(), + }, regularAccounts[0], ); }); diff --git a/test/unit/MGlobalInfiniFiCustomAggregatorFeedGrowth.test.ts b/test/unit/MGlobalInfiniFiCustomAggregatorFeedGrowth.test.ts deleted file mode 100644 index 150daf8b..00000000 --- a/test/unit/MGlobalInfiniFiCustomAggregatorFeedGrowth.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; -import { expect } from 'chai'; -import { parseUnits, solidityKeccak256 } from 'ethers/lib/utils'; -import { ethers, upgrades } from 'hardhat'; - -import { - MGlobalInfiniFiCustomAggregatorFeedGrowth, - MGlobalInfiniFiCustomAggregatorFeedGrowth__factory, - MidasAccessControlTest__factory, -} from '../../typechain-types'; -import { acErrors } from '../common/ac.helpers'; - -const PARAMS = { - minAnswer: parseUnits('0.1', 8), - maxAnswer: parseUnits('2', 8), - maxAnswerDeviation: parseUnits('1', 8), - minGrowthApr: parseUnits('0', 8), - maxGrowthApr: parseUnits('7.24', 8), - onlyUp: true, - description: 'infiniFi MG Yield Oracle', -} as const; - -const EXPECTED_ROLE = solidityKeccak256( - ['string'], - ['INFINIFI_MG_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE'], -); - -const deployFixture = async () => { - const [owner, infinifiAdmin, stranger] = await ethers.getSigners(); - - const accessControl = await new MidasAccessControlTest__factory( - owner, - ).deploy(); - await accessControl.initialize(); - - const feed = (await upgrades.deployProxy( - new MGlobalInfiniFiCustomAggregatorFeedGrowth__factory(owner), - [ - accessControl.address, - PARAMS.minAnswer, - PARAMS.maxAnswer, - PARAMS.maxAnswerDeviation, - PARAMS.minGrowthApr, - PARAMS.maxGrowthApr, - PARAMS.onlyUp, - PARAMS.description, - ], - )) as MGlobalInfiniFiCustomAggregatorFeedGrowth; - - return { owner, infinifiAdmin, stranger, accessControl, feed }; -}; - -describe('MGlobalInfiniFiCustomAggregatorFeedGrowth', () => { - it('initializes with the InfiniFi ticket parameters', async () => { - const { feed } = await loadFixture(deployFixture); - - expect(await feed.description()).eq(PARAMS.description); - expect(await feed.decimals()).eq(8); - expect(await feed.version()).eq(1); - expect(await feed.minAnswer()).eq(PARAMS.minAnswer); - expect(await feed.maxAnswer()).eq(PARAMS.maxAnswer); - expect(await feed.maxAnswerDeviation()).eq(PARAMS.maxAnswerDeviation); - expect(await feed.minGrowthApr()).eq(PARAMS.minGrowthApr); - expect(await feed.maxGrowthApr()).eq(PARAMS.maxGrowthApr); - expect(await feed.onlyUp()).eq(PARAMS.onlyUp); - }); - - it('exposes the dedicated INFINIFI_MG role and the constant matches its keccak preimage', async () => { - const { feed } = await loadFixture(deployFixture); - - expect(await feed.feedAdminRole()).eq(EXPECTED_ROLE); - expect(await feed.INFINIFI_MG_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE()).eq( - EXPECTED_ROLE, - ); - expect(await feed.feedAdminRole()).not.eq( - await feed.M_GLOBAL_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE(), - ); - }); - - it('rejects setRoundData from a caller without the InfiniFi role', async () => { - const { feed, stranger } = await loadFixture(deployFixture); - - const ts = (await ethers.provider.getBlock('latest')).timestamp - 1; - - await expect( - feed.connect(stranger).setRoundData(parseUnits('1', 8), ts, 0), - ).revertedWith(acErrors.WMAC_HASNT_ROLE); - }); - - it('accepts setRoundData from a caller granted the InfiniFi role and seeds $1.00 / 0% APR', async () => { - const { feed, accessControl, owner, infinifiAdmin } = await loadFixture( - deployFixture, - ); - - await accessControl - .connect(owner) - .grantRole(EXPECTED_ROLE, infinifiAdmin.address); - - const ts = (await ethers.provider.getBlock('latest')).timestamp - 1; - - await expect( - feed - .connect(infinifiAdmin) - .setRoundData(parseUnits('1', 8), ts, parseUnits('0', 8)), - ).to.emit(feed, 'AnswerUpdated'); - - const [, answer, , , , growthApr] = await feed.latestRoundDataRaw(); - expect(answer).eq(parseUnits('1', 8)); - expect(growthApr).eq(0); - }); - - it('enforces growth APR bounds: rejects > maxGrowthApr (7.24%)', async () => { - const { feed, accessControl, owner, infinifiAdmin } = await loadFixture( - deployFixture, - ); - - await accessControl - .connect(owner) - .grantRole(EXPECTED_ROLE, infinifiAdmin.address); - - const ts = (await ethers.provider.getBlock('latest')).timestamp - 1; - - await expect( - feed - .connect(infinifiAdmin) - .setRoundData(parseUnits('1', 8), ts, PARAMS.maxGrowthApr.add(1)), - ).revertedWith('CAG: out of [min;max] growth'); - }); - - it('enforces growth APR bounds: rejects negative APR (minGrowthApr = 0)', async () => { - const { feed, accessControl, owner, infinifiAdmin } = await loadFixture( - deployFixture, - ); - - await accessControl - .connect(owner) - .grantRole(EXPECTED_ROLE, infinifiAdmin.address); - - const ts = (await ethers.provider.getBlock('latest')).timestamp - 1; - - await expect( - feed.connect(infinifiAdmin).setRoundData(parseUnits('1', 8), ts, -1), - ).revertedWith('CAG: out of [min;max] growth'); - }); -}); diff --git a/test/unit/MidasAccessControl.test.ts b/test/unit/MidasAccessControl.test.ts index 0288d833..51ad52f3 100644 --- a/test/unit/MidasAccessControl.test.ts +++ b/test/unit/MidasAccessControl.test.ts @@ -1,10 +1,182 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { increase } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; import { constants } from 'ethers'; import { ethers } from 'hardhat'; -import { WithMidasAccessControlTester__factory } from '../../typechain-types'; +import { encodeFnSelector } from '../../helpers/utils'; +import { + MidasAccessControl, + MidasAccessControl__factory, + MidasAccessControlTimelockController, + MidasTimelockManager, + WithMidasAccessControlTester, + WithMidasAccessControlTester__factory, +} from '../../typechain-types'; +import { + acErrors, + grantRoleMultTester, + grantRoleTester, + revokeRoleMultTester, + revokeRoleTester, + setIsUserFacingRoleTester, + setGrantOperatorRoleTester, + setPermissionRoleTester, + setupGrantOperatorRole, + setDefaultDelayTest, + setRoleTimelocksAndExecute, + setRoleTimelocksTester, + NO_DELAY, +} from '../common/ac.helpers'; +import { + handleRevert, + validateImplementation, + asyncForEach, +} from '../common/common.helpers'; +import { deployProxyContract } from '../common/deploy.helpers'; import { defaultDeploy } from '../common/fixtures'; +import { + executeTimelockOperationTester, + bulkScheduleTimelockOperationTester, +} from '../common/timelock-manager.helpers'; + +const withOnlyRoleSelector = encodeFnSelector('withOnlyRole(bytes32,bool)'); +const withOnlyContractAdminSelector = encodeFnSelector( + 'withOnlyContractAdmin()', +); +const withOnlyRoleNoTimelockSelector = encodeFnSelector( + 'withOnlyRoleNoTimelock(bytes32,bool)', +); + +const DELAY_FOR_SET_DEFAULT_DELAY = 2 * 24 * 3600; +const setDefaultDelaySelector = encodeFnSelector('setDefaultDelay(uint256)'); + +const PROXY_ADMIN_SLOT = + '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103'; + +const setProxyAdmin = (address: string, admin: string) => + ethers.provider.send('hardhat_setStorageAt', [ + address, + PROXY_ADMIN_SLOT, + ethers.utils.hexZeroPad(admin, 32), + ]); + +const setInitializedVersion = (address: string, version: number) => + ethers.provider.send('hardhat_setStorageAt', [ + address, + ethers.utils.hexZeroPad('0x00', 32), + ethers.utils.hexZeroPad(ethers.utils.hexlify(version), 32), + ]); + +const timelockManagerRevertOpts = ( + timelockManager: MidasTimelockManager, + customErrorName: string, + args?: unknown[], +) => ({ + revertCustomError: { + contract: timelockManager, + customErrorName, + args, + }, +}); + +const getScopedFunctionKeys = async ( + accessControl: MidasAccessControl, + masterRole: string, + functionSelector: string, + wAccessControlTester: WithMidasAccessControlTester, + timelockManager: MidasTimelockManager, +) => { + const wacFunctionKey = await accessControl.permissionRoleKey( + masterRole, + wAccessControlTester.address, + functionSelector, + ); + const timelockManagerFunctionKey = await accessControl.permissionRoleKey( + masterRole, + timelockManager.address, + functionSelector, + ); + + return { wacFunctionKey, timelockManagerFunctionKey }; +}; + +const setupFunctionPermissionRole = async ( + accessControl: MidasAccessControl, + owner: SignerWithAddress, + wAccessControlTester: WithMidasAccessControlTester, + timelockManager: MidasTimelockManager, + masterRole: string, + functionSelector: string, + account: string, +) => { + await asyncForEach( + [wAccessControlTester.address, timelockManager.address], + async (targetContract) => { + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole, + targetContract, + functionSelector, + grantOperator: owner, + }); + await setPermissionRoleTester( + { accessControl, owner }, + masterRole, + targetContract, + functionSelector, + [{ account, enabled: true }], + ); + }, + true, + ); + + return getScopedFunctionKeys( + accessControl, + masterRole, + functionSelector, + wAccessControlTester, + timelockManager, + ); +}; + +const setupWithOnlyRolePermission = async ( + accessControl: MidasAccessControl, + owner: SignerWithAddress, + wAccessControlTester: WithMidasAccessControlTester, + timelockManager: MidasTimelockManager, + masterRole: string, + account: string, +) => + setupFunctionPermissionRole( + accessControl, + owner, + wAccessControlTester, + timelockManager, + masterRole, + withOnlyRoleSelector, + account, + ); + +const setupWithOnlyContractAdminPermission = async ( + accessControl: MidasAccessControl, + owner: SignerWithAddress, + wAccessControlTester: WithMidasAccessControlTester, + timelockManager: MidasTimelockManager, + masterRole: string, + account: string, +) => + setupFunctionPermissionRole( + accessControl, + owner, + wAccessControlTester, + timelockManager, + masterRole, + withOnlyContractAdminSelector, + account, + ); describe('MidasAccessControl', function () { it('deployment', async () => { @@ -14,16 +186,11 @@ describe('MidasAccessControl', function () { roles.common.blacklistedOperator, roles.common.greenlistedOperator, roles.common.defaultAdmin, - roles.tokenRoles.mTBILL.burner, - roles.tokenRoles.mTBILL.minter, - roles.tokenRoles.mTBILL.pauser, - roles.tokenRoles.mTBILL.redemptionVaultAdmin, - roles.tokenRoles.mTBILL.depositVaultAdmin, ]; - for (const role of initGrantedRoles) { + await asyncForEach(initGrantedRoles, async (role) => { expect(await accessControl.hasRole(role, owner.address)).to.eq(true); - } + }); expect(await accessControl.getRoleAdmin(roles.common.blacklisted)).eq( roles.common.blacklistedOperator, @@ -32,259 +199,3499 @@ describe('MidasAccessControl', function () { expect(await accessControl.getRoleAdmin(roles.common.greenlisted)).eq( roles.common.greenlistedOperator, ); + + expect(await accessControl.isUserFacingRole(roles.common.blacklisted)).eq( + true, + ); + + expect(await accessControl.isUserFacingRole(roles.common.greenlisted)).eq( + true, + ); + + await validateImplementation(MidasAccessControl__factory); }); it('initialize', async () => { const { accessControl } = await loadFixture(defaultDeploy); - await expect(accessControl.initialize()).revertedWith( + await expect(accessControl.initialize(NO_DELAY, [])).revertedWith( 'Initializable: contract is already initialized', ); }); - describe('grantRoleMult()', () => { + describe('initializeV2() proxy admin restriction', () => { + const deployAccessControl = () => + deployProxyContract( + 'MidasAccessControl', + [0, []], + 'initialize', + ); + + it('initializeV2 runs while proxy admin is zero during fresh deploy', async () => { + const accessControl = await deployProxyContract( + 'MidasAccessControl', + [3600, []], + 'initialize', + ); + + expect(await accessControl.defaultDelay()).eq(3600); + }); + + it('should fail: when already reinitialized, even from the proxy admin', async () => { + const [, admin] = await ethers.getSigners(); + const accessControl = await deployAccessControl(); + + await setProxyAdmin(accessControl.address, admin.address); + + await expect( + accessControl.connect(admin).initializeV2(0, []), + ).revertedWith('Initializable: contract is already initialized'); + }); + + it('should fail: when initialized and caller is not the proxy admin', async () => { + const [, admin, stranger] = await ethers.getSigners(); + const accessControl = await deployAccessControl(); + + await setProxyAdmin(accessControl.address, admin.address); + await setInitializedVersion(accessControl.address, 1); + + await expect( + accessControl.connect(stranger).initializeV2(0, []), + ).revertedWithCustomError(accessControl, 'SenderNotProxyAdmin'); + }); + + it('when initialized and caller is the proxy admin', async () => { + const [, admin] = await ethers.getSigners(); + const accessControl = await deployAccessControl(); + + await setProxyAdmin(accessControl.address, admin.address); + await setInitializedVersion(accessControl.address, 1); + + await accessControl.connect(admin).initializeV2(3600, []); + + expect(await accessControl.defaultDelay()).eq(3600); + }); + }); + + describe('renounceRole()', () => { it('should fail: function is forbidden', async () => { const { accessControl } = await loadFixture(defaultDeploy); await expect( accessControl.renounceRole(constants.HashZero, constants.AddressZero), - ).revertedWith('MAC: Forbidden'); + ).revertedWithCustomError(accessControl, 'Forbidden'); }); }); describe('grantRoleMult()', () => { - it('should fail: arrays length mismatch', async () => { - const { accessControl } = await loadFixture(defaultDeploy); + it('should fail: array is empty', async () => { + const { accessControl, owner, roles } = await loadFixture(defaultDeploy); - await expect( - accessControl.grantRoleMult([], [ethers.constants.AddressZero]), - ).revertedWith('MAC: mismatch arrays'); + await grantRoleMultTester({ accessControl, owner }, [], { + revertCustomError: { customErrorName: 'EmptyArray' }, + }); }); - it('should fail: arrays length mismatch', async () => { - const { accessControl, regularAccounts } = await loadFixture( + it('should fail: when user does not have admin role for roles[0]', async () => { + const { accessControl, regularAccounts, roles } = await loadFixture( defaultDeploy, ); - const arr = [ + await grantRoleMultTester( + { accessControl, owner: regularAccounts[0] }, + [ + { + role: roles.common.blacklisted, + account: regularAccounts[1].address, + delay: 0, + }, + ], + { revertCustomError: acErrors.WMAC_HASNT_PERMISSION() }, + ); + }); + + it('should fail: when user has admin role but roles[1] admin role is different fron roles[0]', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + await grantRoleTester( + { accessControl, owner }, + roles.common.blacklistedOperator, + regularAccounts[0].address, + ); + + await grantRoleMultTester( + { accessControl, owner: regularAccounts[0] }, + [ + { + role: roles.common.blacklisted, + account: regularAccounts[1].address, + delay: 0, + }, + { + role: roles.common.greenlisted, + account: regularAccounts[2].address, + delay: 0, + }, + ], + { revertCustomError: { customErrorName: 'RoleAdminMismatch' } }, + ); + }); + + it('when timelock delay is not 0 - schedule and execute the tx', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + timelock, + timelockManager, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [roles.common.blacklistedOperator], + [3600], + ); + + const data = accessControl.interface.encodeFunctionData('grantRoleMult', [ + [ + { + role: roles.common.blacklisted, + account: regularAccounts[0].address, + delay: 0, + }, + ], + ]); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [data], + {}, + { from: owner }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + data, + owner.address, + { from: owner }, + ); + }); + + it('when address already have role (shouldnt fail)', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + await grantRoleMultTester({ accessControl, owner }, [ { - role: await accessControl.BLACKLIST_OPERATOR_ROLE(), - user: regularAccounts[0].address, + role: roles.common.blacklisted, + account: regularAccounts[0].address, + delay: 0, }, + ]); + + await grantRoleMultTester({ accessControl, owner }, [ { - role: await accessControl.GREENLIST_OPERATOR_ROLE(), - user: regularAccounts[0].address, + role: roles.common.blacklisted, + account: regularAccounts[0].address, + delay: 0, }, - ]; + ]); + }); - await expect( - accessControl.grantRoleMult( - arr.map((v) => v.role), - arr.map((v) => v.user), - ), - ).not.reverted; + it('should fail: when user have function access role but do not have role admin role', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); - for (const setRoles of arr) { - expect(await accessControl.hasRole(setRoles.role, setRoles.user)).eq( - true, - ); - } - }); - }); + const selector = encodeFnSelector('grantRoleMult(bytes32[],address[])'); - describe('revokeRoleMult()', () => { - it('should fail: arrays length mismatch', async () => { - const { accessControl } = await loadFixture(defaultDeploy); + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.common.blacklistedOperator, + targetContract: accessControl.address, + functionSelector: selector, + grantOperator: owner, + }); - await expect( - accessControl.revokeRoleMult([], [ethers.constants.AddressZero]), - ).revertedWith('MAC: mismatch arrays'); - }); + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + accessControl.address, + selector, + [{ account: regularAccounts[0].address, enabled: true }], + ); - it('should fail: arrays length mismatch', async () => { - const { accessControl, regularAccounts } = await loadFixture( - defaultDeploy, + await grantRoleMultTester( + { accessControl, owner: regularAccounts[0] }, + [ + { + role: roles.common.blacklisted, + account: regularAccounts[1].address, + delay: 0, + }, + ], + { revertCustomError: acErrors.WMAC_HASNT_PERMISSION() }, ); + }); + + it('when delay is not NULL_DELAY but actual delay is NULL_DELAY - should set the delay', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); - const arr = [ + await grantRoleMultTester({ accessControl, owner }, [ { - role: await accessControl.BLACKLIST_OPERATOR_ROLE(), - user: regularAccounts[0].address, + role: roles.common.blacklisted, + account: regularAccounts[0].address, + delay: 3600, }, + ]); + }); + + it('should fail: when delay is not NULL_DELAY but actual delay is also not null', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + await grantRoleMultTester({ accessControl, owner }, [ { - role: await accessControl.GREENLIST_OPERATOR_ROLE(), - user: regularAccounts[0].address, + role: roles.common.blacklisted, + account: regularAccounts[0].address, + delay: 3600, }, - ]; + ]); - await expect( - accessControl.grantRoleMult( - arr.map((v) => v.role), - arr.map((v) => v.user), - ), - ).not.reverted; - await expect( - accessControl.revokeRoleMult( - arr.map((v) => v.role), - arr.map((v) => v.user), - ), - ).not.reverted; + await grantRoleMultTester( + { accessControl, owner }, + [ + { + role: roles.common.blacklisted, + account: regularAccounts[1].address, + delay: 7200, + }, + ], + { revertCustomError: { customErrorName: 'DelayIsAlreadySet' } }, + ); + }); - for (const setRoles of arr) { - expect(await accessControl.hasRole(setRoles.role, setRoles.user)).eq( - false, - ); - } + it('should fail: when array contains 2 identical roles and both tries to update the delay, and actual delay is NULL_DELAY', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + await grantRoleMultTester( + { accessControl, owner }, + [ + { + role: roles.common.blacklisted, + account: regularAccounts[0].address, + delay: 3600, + }, + { + role: roles.common.blacklisted, + account: regularAccounts[1].address, + delay: 3600, + }, + ], + { revertCustomError: { customErrorName: 'DelayIsAlreadySet' } }, + ); }); }); -}); -describe('WithMidasAccessControl', function () { - it('deployment', async () => { - const { accessControl, wAccessControlTester } = await loadFixture( - defaultDeploy, - ); - expect(await wAccessControlTester.accessControl()).eq( - accessControl.address, - ); - }); + describe('revokeRoleMult()', () => { + it('should fail: array is empty', async () => { + const { accessControl, owner } = await loadFixture(defaultDeploy); - it('onlyInitializing', async () => { - const { accessControl, owner } = await loadFixture(defaultDeploy); + await revokeRoleMultTester({ accessControl, owner }, [], { + revertCustomError: { customErrorName: 'EmptyArray' }, + }); + }); - const wac = await new WithMidasAccessControlTester__factory(owner).deploy(); + it('should fail: when user does not have admin role for roles[0]', async () => { + const { accessControl, regularAccounts, roles } = await loadFixture( + defaultDeploy, + ); - await expect( - wac.initializeWithoutInitializer(accessControl.address), - ).revertedWith('Initializable: contract is not initializing'); - }); + await revokeRoleMultTester( + { accessControl, owner: regularAccounts[0] }, + [ + { + role: roles.common.blacklisted, + account: regularAccounts[1].address, + }, + ], + { revertCustomError: acErrors.WMAC_HASNT_PERMISSION() }, + ); + }); - describe('modifier onlyRole', () => { - it('should fail when call from non DEFAULT_ADMIN_ROLE address', async () => { - const { wAccessControlTester, regularAccounts, roles } = + it('should fail: when user has admin role but roles[1] admin role is different fron roles[0]', async () => { + const { accessControl, owner, regularAccounts, roles } = await loadFixture(defaultDeploy); - await expect( - wAccessControlTester - .connect(regularAccounts[1]) - .withOnlyRole(roles.common.blacklisted, regularAccounts[0].address), - ).revertedWith('WMAC: hasnt role'); + + await grantRoleTester( + { accessControl, owner }, + roles.common.blacklistedOperator, + regularAccounts[0].address, + ); + + await revokeRoleMultTester( + { accessControl, owner: regularAccounts[0] }, + [ + { + role: roles.common.blacklisted, + account: regularAccounts[1].address, + }, + { + role: roles.common.greenlisted, + account: regularAccounts[2].address, + }, + ], + { revertCustomError: { customErrorName: 'RoleAdminMismatch' } }, + ); }); - it('call from DEFAULT_ADMIN_ROLE address', async () => { - const { wAccessControlTester, owner, roles } = await loadFixture( - defaultDeploy, + it('should fail: when trying to revoke DEFAULT_ADMIN_ROLE from self', async () => { + const { accessControl, owner, roles } = await loadFixture(defaultDeploy); + + await revokeRoleMultTester( + { accessControl, owner }, + [{ role: roles.common.defaultAdmin, account: owner.address }], + { revertCustomError: { customErrorName: 'CannotRevokeFromSelf' } }, ); - await expect( - wAccessControlTester.withOnlyRole( - roles.common.blacklistedOperator, - owner.address, - ), - ).not.reverted; }); - }); - describe('modifier onlyNotRole', () => { - it('should fail when call from DEFAULT_ADMIN_ROLE address', async () => { - const { wAccessControlTester, owner, roles } = await loadFixture( - defaultDeploy, + it('when revoking role from self but its not DEFAULT_ADMIN_ROLE (should not fail)', async () => { + const { accessControl, owner, roles } = await loadFixture(defaultDeploy); + + await revokeRoleMultTester({ accessControl, owner }, [ + { role: roles.common.blacklistedOperator, account: owner.address }, + ]); + }); + + it('should fail: when revoking DEFAULT_ADMIN_ROLE from self and timelock delay is not 0', async () => { + // Locks in that _validateRevokeRole uses the resolved proposer (actualSender), + // not msg.sender. If it compared against the timelock address, self-revoke + // via timelock would incorrectly succeed. + const { accessControl, owner, roles, timelock, timelockManager } = + await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [roles.common.defaultAdmin], + [3600], ); - await expect( - wAccessControlTester.withOnlyNotRole( - roles.common.blacklistedOperator, - owner.address, - ), - ).revertedWith('WMAC: has role'); + + const data = accessControl.interface.encodeFunctionData( + 'revokeRoleMult', + [[{ role: roles.common.defaultAdmin, account: owner.address }]], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [data], + {}, + { from: owner }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + data, + owner.address, + { + from: owner, + revertMessage: 'TimelockController: underlying transaction reverted', + }, + ); + + expect( + await accessControl.hasRole(roles.common.defaultAdmin, owner.address), + ).eq(true); }); - it('call from non DEFAULT_ADMIN_ROLE address', async () => { - const { wAccessControlTester, regularAccounts, roles } = + it('when timelock delay is not 0 - schedule and execute the tx', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + timelock, + timelockManager, + } = await loadFixture(defaultDeploy); + + await grantRoleMultTester({ accessControl, owner }, [ + { role: roles.common.blacklisted, account: regularAccounts[0].address }, + ]); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [roles.common.blacklistedOperator], + [3600], + ); + + const data = accessControl.interface.encodeFunctionData( + 'revokeRoleMult', + [ + [ + { + role: roles.common.blacklisted, + account: regularAccounts[0].address, + }, + ], + ], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [data], + {}, + { from: owner }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + data, + owner.address, + { from: owner }, + ); + }); + + it('should fail: when user have function access role but do not have role admin role', async () => { + const { accessControl, owner, regularAccounts, roles } = await loadFixture(defaultDeploy); - await expect( - wAccessControlTester.withOnlyNotRole( - roles.common.blacklisted, - regularAccounts[1].address, - ), - ).not.reverted; + + const selector = encodeFnSelector('revokeRoleMult(bytes32[],address[])'); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.common.blacklistedOperator, + targetContract: accessControl.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + accessControl.address, + selector, + [{ account: regularAccounts[0].address, enabled: true }], + ); + + await revokeRoleMultTester( + { accessControl, owner: regularAccounts[0] }, + [ + { + role: roles.common.blacklisted, + account: regularAccounts[1].address, + }, + ], + { revertCustomError: acErrors.WMAC_HASNT_PERMISSION() }, + ); + }); + + it('when address do not have the role (shouldnt fail)', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + await revokeRoleMultTester({ accessControl, owner }, [ + { role: roles.common.blacklisted, account: regularAccounts[0].address }, + ]); }); }); - describe('grantRole()', () => { - it('should fail when call from non role admin', async () => { - const { wAccessControlTester, accessControl, regularAccounts, roles } = + describe('setRoleAdmin()', () => { + it('should fail: caller does not have `DEFAULT_ADMIN_ROLE`', async () => { + const { accessControl, regularAccounts, roles } = await loadFixture( + defaultDeploy, + ); + + await handleRevert( + accessControl + .connect(regularAccounts[0]) + .setRoleAdmin.bind( + this, + roles.common.blacklisted, + roles.common.greenlistedOperator, + ), + accessControl, + { revertCustomError: acErrors.WMAC_HASNT_PERMISSION() }, + ); + }); + + it('should fail: caller has admin role for another role', async () => { + const { accessControl, regularAccounts, owner, roles } = await loadFixture(defaultDeploy); - expect( - await accessControl.hasRole( - roles.common.blacklistedOperator, - wAccessControlTester.address, - ), - ).eq(false); + + await grantRoleTester( + { accessControl, owner }, + roles.common.greenlistedOperator, + regularAccounts[0].address, + ); + await expect( - wAccessControlTester.grantRoleTester( - roles.common.blacklisted, - regularAccounts[1].address, - ), + accessControl + .connect(regularAccounts[0]) + .setRoleAdmin( + roles.common.blacklisted, + roles.common.greenlistedOperator, + ), ).reverted; }); - it('call from role admin', async () => { - const { accessControl, wAccessControlTester, regularAccounts, roles } = + it('caller has current role admin but not the DEFAULT_ADMIN_ROLE', async () => { + const { accessControl, roles, owner, regularAccounts } = await loadFixture(defaultDeploy); - await accessControl.grantRole( + + await grantRoleTester( + { accessControl, owner }, roles.common.blacklistedOperator, - wAccessControlTester.address, + regularAccounts[0].address, ); + await expect( - wAccessControlTester.grantRoleTester( - roles.common.blacklisted, - regularAccounts[1].address, - ), + accessControl + .connect(regularAccounts[0]) + .setRoleAdmin( + roles.common.blacklisted, + roles.common.greenlistedOperator, + ), ).not.reverted; }); - }); - describe('revokeRole()', () => { - it('should fail when call from non role admin', async () => { - const { wAccessControlTester, accessControl, regularAccounts, roles } = - await loadFixture(defaultDeploy); - expect( - await accessControl.hasRole( - roles.common.blacklistedOperator, - wAccessControlTester.address, - ), - ).eq(false); + it('should fail: caller has DEFAULT_ADMIN_ROLE but not current role admin', async () => { + const { accessControl, owner, roles } = await loadFixture(defaultDeploy); + + await accessControl.revokeRole( + roles.common.blacklistedOperator, + owner.address, + ); + await expect( - wAccessControlTester.revokeRoleTester( + accessControl.setRoleAdmin( roles.common.blacklisted, - regularAccounts[1].address, + roles.common.greenlistedOperator, ), - ).reverted; + ).revertedWithCustomError( + accessControl, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); }); - it('call from role admin', async () => { - const { accessControl, wAccessControlTester, regularAccounts, roles } = + it('should use new role admin for role management', async () => { + const { accessControl, owner, regularAccounts, roles } = await loadFixture(defaultDeploy); - await accessControl.grantRole( + + const NEW_ADMIN_ROLE = ethers.utils.id('NEW_ADMIN_ROLE'); + const TEST_ROLE = ethers.utils.id('TEST_ROLE'); + + await accessControl.setRoleAdmin( + NEW_ADMIN_ROLE, roles.common.blacklistedOperator, - wAccessControlTester.address, ); - await wAccessControlTester.grantRoleTester( - roles.common.blacklisted, + + await grantRoleTester( + { accessControl, owner }, + roles.common.blacklistedOperator, + regularAccounts[0].address, + ); + await grantRoleTester( + { accessControl, owner }, + NEW_ADMIN_ROLE, regularAccounts[1].address, ); - await expect( - wAccessControlTester.revokeRoleTester( - roles.common.blacklisted, - regularAccounts[1].address, - ), - ).not.reverted; + await accessControl.setRoleAdmin(TEST_ROLE, NEW_ADMIN_ROLE); + + await grantRoleTester( + { accessControl, owner }, + TEST_ROLE, + regularAccounts[2].address, + undefined, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + + await grantRoleTester( + { accessControl, owner }, + TEST_ROLE, + regularAccounts[2].address, + undefined, + { + from: regularAccounts[1], + }, + ); expect( - await accessControl.hasRole( - roles.common.blacklisted, - regularAccounts[1].address, - ), - ).eq(false); + await accessControl.hasRole(TEST_ROLE, regularAccounts[2].address), + ).eq(true); + }); + + it('when timelock delay is not 0 - schedule and execute the tx', async () => { + const { accessControl, owner, roles, timelock, timelockManager } = + await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [roles.common.blacklistedOperator], + [3600], + ); + + const data = accessControl.interface.encodeFunctionData('setRoleAdmin', [ + roles.common.blacklisted, + roles.common.greenlistedOperator, + ]); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [data], + {}, + { from: owner }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + data, + owner.address, + { from: owner }, + ); + }); + + it('should fail: when user have function access role but do not have role admin role', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setRoleAdmin(bytes32,bytes32)'); + + await wAccessControlTester.setContractAdminRole( + roles.common.blacklistedOperator, + ); + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.common.blacklistedOperator, + targetContract: wAccessControlTester.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + roles.common.blacklistedOperator, + wAccessControlTester.address, + selector, + [{ account: regularAccounts[0].address, enabled: true }], + ); + + await expect( + accessControl + .connect(regularAccounts[0]) + .setRoleAdmin( + roles.common.blacklisted, + roles.common.greenlistedOperator, + ), + ).revertedWithCustomError( + accessControl, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + }); + + describe('setUserFacingRoleMult()', () => { + it('should fail: non-DEFAULT_ADMIN reverts', async () => { + const { accessControl, regularAccounts, roles } = await loadFixture( + defaultDeploy, + ); + + await setIsUserFacingRoleTester( + { + accessControl, + owner: regularAccounts[0], + }, + [ + { + role: roles.common.greenlistedOperator, + enabled: true, + }, + ], + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('call from DEFAULT_ADMIN_ROLE', async () => { + const { accessControl, owner, roles } = await loadFixture(defaultDeploy); + + await setIsUserFacingRoleTester( + { + accessControl, + owner, + }, + [ + { + role: roles.common.greenlistedOperator, + enabled: true, + }, + ], + ); + }); + + it('when already user facing (should not revert)', async () => { + const { accessControl, owner, roles } = await loadFixture(defaultDeploy); + + await setIsUserFacingRoleTester({ accessControl, owner }, [ + { role: roles.common.blacklisted, enabled: true }, + ]); + }); + + it('when already non-userfacing (should not revert)', async () => { + const { accessControl, owner, roles } = await loadFixture(defaultDeploy); + + await setIsUserFacingRoleTester({ accessControl, owner }, [ + { role: roles.common.blacklistedOperator, enabled: false }, + ]); + }); + + it('when timelock delay is not 0 - schedule and execute the tx', async () => { + const { accessControl, owner, roles, timelock, timelockManager } = + await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [roles.common.defaultAdmin], + [3600], + ); + + const data = accessControl.interface.encodeFunctionData( + 'setUserFacingRoleMult', + [[{ role: roles.common.blacklistedOperator, enabled: true }]], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [data], + {}, + { from: owner }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + data, + owner.address, + { from: owner }, + ); + }); + + it('should fail: when user have function access role but do not have role admin role', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + const selector = encodeFnSelector( + 'setUserFacingRoleMult((bytes32,bool)[])', + ); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.common.defaultAdmin, + targetContract: accessControl.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + accessControl.address, + selector, + [{ account: regularAccounts[0].address, enabled: true }], + ); + + await setIsUserFacingRoleTester( + { accessControl, owner: regularAccounts[0] }, + [{ role: roles.common.blacklistedOperator, enabled: true }], + { revertCustomError: acErrors.WMAC_HASNT_PERMISSION() }, + ); + }); + + it('should fail: when params lenght is 0', async () => { + const { accessControl, owner } = await loadFixture(defaultDeploy); + + await setIsUserFacingRoleTester({ accessControl, owner }, [], { + revertCustomError: { customErrorName: 'EmptyArray' }, + }); + }); + + it('when switching enabled to disabled', async () => { + const { accessControl, owner, roles } = await loadFixture(defaultDeploy); + + await setIsUserFacingRoleTester({ accessControl, owner }, [ + { role: roles.common.greenlistedOperator, enabled: true }, + ]); + + await setIsUserFacingRoleTester({ accessControl, owner }, [ + { role: roles.common.greenlistedOperator, enabled: false }, + ]); + }); + }); + + describe('setGrantOperatorRoleMult()', () => { + it('should fail: reverts when role is user facing role', async () => { + const { accessControl, owner, wAccessControlTester, roles } = + await loadFixture(defaultDeploy); + + await wAccessControlTester.setContractAdminRole(roles.common.greenlisted); + await setGrantOperatorRoleTester( + { + accessControl, + owner, + }, + wAccessControlTester.address, + [ + { + functionSelector: encodeFnSelector('setGreenlistEnable(bool)'), + operator: owner.address, + enabled: true, + }, + ], + { + revertCustomError: { + customErrorName: 'UserFacingRoleNotAllowed', + }, + }, + ); + }); + + it('when role is not user facing role', async () => { + const { accessControl, owner, roles, wAccessControlTester } = + await loadFixture(defaultDeploy); + + await wAccessControlTester.setContractAdminRole( + roles.common.greenlistedOperator, + ); + + await setGrantOperatorRoleTester( + { + accessControl, + owner, + }, + wAccessControlTester.address, + [ + { + functionSelector: encodeFnSelector('setGreenlistEnable(bool)'), + operator: owner.address, + enabled: true, + }, + ], + ); + }); + + it('when address is already grant operator (should not revert)', async () => { + const { accessControl, owner, roles, wAccessControlTester } = + await loadFixture(defaultDeploy); + + const params = [ + { + functionSelector: encodeFnSelector('setGreenlistEnable(bool)'), + operator: owner.address, + enabled: true, + }, + ]; + + await wAccessControlTester.setContractAdminRole( + roles.common.greenlistedOperator, + ); + + await setGrantOperatorRoleTester( + { accessControl, owner }, + wAccessControlTester.address, + params, + ); + + await setGrantOperatorRoleTester( + { accessControl, owner }, + wAccessControlTester.address, + params, + ); + }); + + it('when timelock delay is not 0 - schedule and execute the tx', async () => { + const { + accessControl, + owner, + roles, + timelock, + timelockManager, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [roles.common.greenlistedOperator], + [3600], + ); + + await wAccessControlTester.setContractAdminRole( + roles.common.greenlistedOperator, + ); + const data = accessControl.interface.encodeFunctionData( + 'setGrantOperatorRoleMult', + [ + wAccessControlTester.address, + [ + { + delay: 0, + functionSelector: encodeFnSelector('setGreenlistEnable(bool)'), + operator: owner.address, + enabled: true, + }, + ], + ], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [data], + {}, + { from: owner }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + data, + owner.address, + { from: owner }, + ); + }); + + it('should fail: when user have function access role but do not have masterRole', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector( + 'setGrantOperatorRoleMult(bytes32,(address,bytes4,address,bool)[])', + ); + await wAccessControlTester.setContractAdminRole( + roles.common.greenlistedOperator, + ); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.common.greenlistedOperator, + targetContract: wAccessControlTester.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + wAccessControlTester.address, + selector, + [{ account: regularAccounts[0].address, enabled: true }], + ); + + await setGrantOperatorRoleTester( + { accessControl, owner: regularAccounts[0] }, + wAccessControlTester.address, + [ + { + functionSelector: encodeFnSelector('setGreenlistEnable(bool)'), + operator: regularAccounts[1].address, + enabled: true, + }, + ], + { revertCustomError: acErrors.WMAC_HASNT_PERMISSION() }, + ); + }); + + it('should fail: when params lenght is 0', async () => { + const { accessControl, owner, wAccessControlTester } = await loadFixture( + defaultDeploy, + ); + + await setGrantOperatorRoleTester( + { accessControl, owner }, + wAccessControlTester.address, + [], + { + revertCustomError: { customErrorName: 'EmptyArray' }, + }, + ); + }); + + it('when switching enabled to disabled', async () => { + const { accessControl, owner, roles, wAccessControlTester } = + await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + + await wAccessControlTester.setContractAdminRole( + roles.common.greenlistedOperator, + ); + + const params = [ + { + functionSelector: selector, + operator: owner.address, + enabled: true, + }, + ]; + + await setGrantOperatorRoleTester( + { accessControl, owner }, + wAccessControlTester.address, + params, + ); + + await setGrantOperatorRoleTester( + { accessControl, owner }, + wAccessControlTester.address, + [{ ...params[0], enabled: false }], + ); + }); + + it('when delay is not NULL_DELAY but actual delay is NULL_DELAY - should set the delay', async () => { + const { accessControl, owner, roles, wAccessControlTester } = + await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + + await wAccessControlTester.setContractAdminRole( + roles.common.greenlistedOperator, + ); + + await setGrantOperatorRoleTester( + { accessControl, owner }, + wAccessControlTester.address, + [ + { + functionSelector: selector, + operator: owner.address, + enabled: true, + delay: 3600, + }, + ], + ); + }); + + it('should fail: when delay is not NULL_DELAY but actual delay is also not null', async () => { + const { + accessControl, + owner, + roles, + timelock, + timelockManager, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + + await wAccessControlTester.setContractAdminRole( + roles.common.greenlistedOperator, + ); + + const operatorRoleKey = await accessControl.grantOperatorRoleKey( + roles.common.greenlistedOperator, + wAccessControlTester.address, + selector, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [operatorRoleKey], + [3600], + ); + + await setGrantOperatorRoleTester( + { accessControl, owner }, + wAccessControlTester.address, + [ + { + functionSelector: selector, + operator: owner.address, + enabled: true, + delay: 7200, + }, + ], + { revertCustomError: { customErrorName: 'DelayIsAlreadySet' } }, + ); + }); + }); + + describe('setPermissionRoleMult()', () => { + it('when caller is function operator', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + await wAccessControlTester.setContractAdminRole( + roles.common.greenlistedOperator, + ); + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.common.greenlistedOperator, + targetContract: wAccessControlTester.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + wAccessControlTester.address, + selector, + [ + { + account: regularAccounts[0].address, + enabled: true, + }, + ], + ); + }); + + it('extracts master role from target contract when not provided', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + const contractAdminRole = roles.common.greenlistedOperator; + + await wAccessControlTester.setContractAdminRole(contractAdminRole); + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: contractAdminRole, + targetContract: wAccessControlTester.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + wAccessControlTester.address, + selector, + [{ account: regularAccounts[0].address, enabled: true }], + ); + + expect( + await accessControl[ + 'hasFunctionPermission(bytes32,address,bytes4,address)' + ]( + contractAdminRole, + wAccessControlTester.address, + selector, + regularAccounts[0].address, + ), + ).eq(true); + expect( + await accessControl[ + 'hasFunctionPermission(bytes32,address,bytes4,address)' + ]( + roles.common.defaultAdmin, + wAccessControlTester.address, + selector, + regularAccounts[0].address, + ), + ).eq(false); + }); + + it('should fail: caller is not a grant operator', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.common.greenlistedOperator, + targetContract: accessControl.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner: regularAccounts[1] }, + undefined, + accessControl.address, + selector, + [ + { + account: regularAccounts[2].address, + enabled: true, + }, + ], + undefined, + { revertCustomError: acErrors.WMAC_HASNT_PERMISSION() }, + ); + }); + + it('should fail: caller is an operator for a different function', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.common.greenlistedOperator, + targetContract: accessControl.address, + functionSelector: encodeFnSelector('setGreenlistEnable1(bool)'), + grantOperator: owner, + }); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + + await grantRoleTester( + { accessControl, owner }, + roles.common.defaultAdmin, + regularAccounts[2], + ); + + await revokeRoleTester( + { accessControl, owner }, + roles.common.defaultAdmin, + owner, + { + from: regularAccounts[2], + }, + ); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + accessControl.address, + selector, + [ + { + account: regularAccounts[2].address, + enabled: true, + }, + ], + undefined, + { revertCustomError: acErrors.WMAC_HASNT_PERMISSION() }, + ); + }); + + it('when address is already has permission (shouldnt fail)', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + + await wAccessControlTester.setContractAdminRole( + roles.common.greenlistedOperator, + ); + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.common.greenlistedOperator, + targetContract: wAccessControlTester.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + wAccessControlTester.address, + selector, + [{ account: regularAccounts[0].address, enabled: true }], + ); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + wAccessControlTester.address, + selector, + [{ account: regularAccounts[0].address, enabled: true }], + ); + }); + + it('should fail: when params lenght is 0', async () => { + const { accessControl, owner, roles } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.common.greenlistedOperator, + targetContract: accessControl.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + accessControl.address, + selector, + [], + undefined, + { revertCustomError: { customErrorName: 'EmptyArray' } }, + ); + }); + + it('should fail: when user do not have grant operator role', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.common.greenlistedOperator, + targetContract: accessControl.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner: regularAccounts[0] }, + undefined, + accessControl.address, + selector, + [{ account: regularAccounts[1].address, enabled: true }], + undefined, + { revertCustomError: acErrors.WMAC_HASNT_PERMISSION() }, + ); + }); + + it('when switching enabled to disabled', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + + await wAccessControlTester.setContractAdminRole( + roles.common.greenlistedOperator, + ); + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.common.greenlistedOperator, + targetContract: wAccessControlTester.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + wAccessControlTester.address, + selector, + [{ account: regularAccounts[0].address, enabled: true }], + ); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + wAccessControlTester.address, + selector, + [{ account: regularAccounts[0].address, enabled: false }], + ); + }); + + it('when delay is not NULL_DELAY but actual delay is NULL_DELAY - should set the delay', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + + await wAccessControlTester.setContractAdminRole( + roles.common.greenlistedOperator, + ); + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.common.greenlistedOperator, + targetContract: wAccessControlTester.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + wAccessControlTester.address, + selector, + [{ account: regularAccounts[0].address, enabled: true }], + 3600, + ); + }); + + it('should fail: when delay is not NULL_DELAY but actual delay is also not null', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + + await wAccessControlTester.setContractAdminRole( + roles.common.greenlistedOperator, + ); + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.common.greenlistedOperator, + targetContract: wAccessControlTester.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + wAccessControlTester.address, + selector, + [{ account: regularAccounts[0].address, enabled: true }], + 3600, + ); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + wAccessControlTester.address, + selector, + [{ account: regularAccounts[1].address, enabled: true }], + 7200, + { revertCustomError: { customErrorName: 'DelayIsAlreadySet' } }, + ); + }); + + describe('without timelock', () => { + it('when caller has master role but not operator role - uses master role', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + const masterRole = roles.common.greenlistedOperator; + + await wAccessControlTester.setContractAdminRole(masterRole); + + expect(await accessControl.hasRole(masterRole, owner.address)).eq(true); + expect( + await accessControl[ + 'isFunctionAccessGrantOperator(bytes32,address,bytes4,address)' + ](masterRole, wAccessControlTester.address, selector, owner.address), + ).eq(false); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + wAccessControlTester.address, + selector, + [{ account: regularAccounts[0].address, enabled: true }], + ); + }); + + it('when caller has operator role but not master role - uses operator role', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + timelock, + timelockManager, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + const masterRole = roles.common.greenlistedOperator; + const operator = regularAccounts[0]; + + await wAccessControlTester.setContractAdminRole(masterRole); + + await setGrantOperatorRoleTester( + { accessControl, owner }, + wAccessControlTester.address, + [ + { + functionSelector: selector, + operator: operator.address, + enabled: true, + }, + ], + ); + + const operatorRoleKey = await accessControl.grantOperatorRoleKey( + masterRole, + wAccessControlTester.address, + selector, + ); + + // master has delay so direct call would fail if master role were wrongly selected + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [masterRole], + [3600], + ); + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [operatorRoleKey], + [NO_DELAY], + ); + + expect(await accessControl.hasRole(masterRole, operator.address)).eq( + false, + ); + expect( + await accessControl[ + 'isFunctionAccessGrantOperator(bytes32,address,bytes4,address)' + ]( + masterRole, + wAccessControlTester.address, + selector, + operator.address, + ), + ).eq(true); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + wAccessControlTester.address, + selector, + [{ account: regularAccounts[1].address, enabled: true }], + undefined, + { from: operator }, + ); + }); + + it('when caller has both roles and master has no delay but operator does - uses master role', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + timelock, + timelockManager, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + const masterRole = roles.common.greenlistedOperator; + + await wAccessControlTester.setContractAdminRole(masterRole); + + const operatorRoleKey = await accessControl.grantOperatorRoleKey( + masterRole, + wAccessControlTester.address, + selector, + ); + + await setGrantOperatorRoleTester( + { accessControl, owner }, + wAccessControlTester.address, + [ + { + functionSelector: selector, + operator: owner.address, + enabled: true, + }, + ], + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [operatorRoleKey], + [3600], + ); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + wAccessControlTester.address, + selector, + [{ account: regularAccounts[0].address, enabled: true }], + ); + }); + + it('when caller has both roles and operator has no delay but master does - uses operator role', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + timelock, + timelockManager, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + const masterRole = roles.common.greenlistedOperator; + + await wAccessControlTester.setContractAdminRole(masterRole); + + const operatorRoleKey = await accessControl.grantOperatorRoleKey( + masterRole, + wAccessControlTester.address, + selector, + ); + + await setGrantOperatorRoleTester( + { accessControl, owner }, + wAccessControlTester.address, + [ + { + functionSelector: selector, + operator: owner.address, + enabled: true, + }, + ], + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [masterRole], + [3600], + ); + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [operatorRoleKey], + [NO_DELAY], + ); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + wAccessControlTester.address, + selector, + [{ account: regularAccounts[0].address, enabled: true }], + ); + }); + }); + + describe('with timelock', () => { + const encodeSetPermissionRoleMult = ( + accessControl: MidasAccessControl, + target: string, + selector: string, + account: string, + ) => + accessControl.interface.encodeFunctionData( + 'setPermissionRoleMult(address,bytes4,uint32,(address,bool)[])', + [target, selector, 0, [{ account, enabled: true }]], + ); + + const scheduleAndExecuteSetPermissionRoleMult = async ( + { + accessControl, + owner, + timelock, + timelockManager, + }: { + accessControl: MidasAccessControl; + owner: SignerWithAddress; + timelock: MidasAccessControlTimelockController; + timelockManager: MidasTimelockManager; + }, + data: string, + delay: number, + from: SignerWithAddress, + ) => { + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [data], + {}, + { from }, + ); + + await increase(delay); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + data, + from.address, + { from: owner }, + ); + }; + + it('when caller has master role but not operator role - schedule and execute', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + timelock, + timelockManager, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + const masterRole = roles.common.greenlistedOperator; + const delay = 3600; + const permissionAccount = regularAccounts[0].address; + + await wAccessControlTester.setContractAdminRole(masterRole); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [masterRole], + [delay], + ); + + expect(await accessControl.hasRole(masterRole, owner.address)).eq(true); + expect( + await accessControl[ + 'isFunctionAccessGrantOperator(bytes32,address,bytes4,address)' + ](masterRole, wAccessControlTester.address, selector, owner.address), + ).eq(false); + + const data = encodeSetPermissionRoleMult( + accessControl, + wAccessControlTester.address, + selector, + permissionAccount, + ); + + const [roleUsed] = await timelockManager.getTargetRole( + accessControl.address, + data, + owner.address, + ); + expect(roleUsed).eq(masterRole); + + await scheduleAndExecuteSetPermissionRoleMult( + { accessControl, owner, timelock, timelockManager }, + data, + delay, + owner, + ); + + expect( + await accessControl[ + 'hasFunctionPermission(bytes32,address,bytes4,address)' + ]( + masterRole, + wAccessControlTester.address, + selector, + permissionAccount, + ), + ).eq(true); + }); + + it('when caller has operator role but not master role - schedule and execute', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + timelock, + timelockManager, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + const masterRole = roles.common.greenlistedOperator; + const operator = regularAccounts[0]; + const delay = 3600; + const permissionAccount = regularAccounts[1].address; + + await wAccessControlTester.setContractAdminRole(masterRole); + + await setGrantOperatorRoleTester( + { accessControl, owner }, + wAccessControlTester.address, + [ + { + functionSelector: selector, + operator: operator.address, + enabled: true, + }, + ], + ); + + const operatorRoleKey = await accessControl.grantOperatorRoleKey( + masterRole, + wAccessControlTester.address, + selector, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [operatorRoleKey], + [delay], + ); + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [masterRole], + [7200], + ); + + expect(await accessControl.hasRole(masterRole, operator.address)).eq( + false, + ); + expect( + await accessControl[ + 'isFunctionAccessGrantOperator(bytes32,address,bytes4,address)' + ]( + masterRole, + wAccessControlTester.address, + selector, + operator.address, + ), + ).eq(true); + + const data = encodeSetPermissionRoleMult( + accessControl, + wAccessControlTester.address, + selector, + permissionAccount, + ); + + const [roleUsed] = await timelockManager.getTargetRole( + accessControl.address, + data, + operator.address, + ); + expect(roleUsed).eq(operatorRoleKey); + + await scheduleAndExecuteSetPermissionRoleMult( + { accessControl, owner, timelock, timelockManager }, + data, + delay, + operator, + ); + + expect( + await accessControl[ + 'hasFunctionPermission(bytes32,address,bytes4,address)' + ]( + masterRole, + wAccessControlTester.address, + selector, + permissionAccount, + ), + ).eq(true); + }); + + it('when caller has both roles with the same delay - schedule and execute', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + timelock, + timelockManager, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + const masterRole = roles.common.greenlistedOperator; + const delay = 3600; + const permissionAccount = regularAccounts[0].address; + + await wAccessControlTester.setContractAdminRole(masterRole); + + const operatorRoleKey = await accessControl.grantOperatorRoleKey( + masterRole, + wAccessControlTester.address, + selector, + ); + + await setGrantOperatorRoleTester( + { accessControl, owner }, + wAccessControlTester.address, + [ + { + functionSelector: selector, + operator: owner.address, + enabled: true, + }, + ], + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [operatorRoleKey, masterRole], + [delay, delay], + ); + + const data = encodeSetPermissionRoleMult( + accessControl, + wAccessControlTester.address, + selector, + permissionAccount, + ); + + // equal delays → resolveAccessRole prefers master (rootDelay <= functionDelay) + const [roleUsed] = await timelockManager.getTargetRole( + accessControl.address, + data, + owner.address, + ); + expect(roleUsed).eq(masterRole); + + await scheduleAndExecuteSetPermissionRoleMult( + { accessControl, owner, timelock, timelockManager }, + data, + delay, + owner, + ); + + expect( + await accessControl[ + 'hasFunctionPermission(bytes32,address,bytes4,address)' + ]( + masterRole, + wAccessControlTester.address, + selector, + permissionAccount, + ), + ).eq(true); + }); + + it('when caller has both roles and master has shorter delay - uses master role delay', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + timelock, + timelockManager, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + const masterRole = roles.common.greenlistedOperator; + const masterDelay = 3600; + const operatorDelay = 7200; + const permissionAccount = regularAccounts[0].address; + + await wAccessControlTester.setContractAdminRole(masterRole); + + const operatorRoleKey = await accessControl.grantOperatorRoleKey( + masterRole, + wAccessControlTester.address, + selector, + ); + + await setGrantOperatorRoleTester( + { accessControl, owner }, + wAccessControlTester.address, + [ + { + functionSelector: selector, + operator: owner.address, + enabled: true, + }, + ], + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [masterRole, operatorRoleKey], + [masterDelay, operatorDelay], + ); + + const data = encodeSetPermissionRoleMult( + accessControl, + wAccessControlTester.address, + selector, + permissionAccount, + ); + + const [roleUsed] = await timelockManager.getTargetRole( + accessControl.address, + data, + owner.address, + ); + expect(roleUsed).eq(masterRole); + + await scheduleAndExecuteSetPermissionRoleMult( + { accessControl, owner, timelock, timelockManager }, + data, + masterDelay, + owner, + ); + + expect( + await accessControl[ + 'hasFunctionPermission(bytes32,address,bytes4,address)' + ]( + masterRole, + wAccessControlTester.address, + selector, + permissionAccount, + ), + ).eq(true); + }); + + it('when caller has both roles and operator has shorter delay - uses operator role delay', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + timelock, + timelockManager, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const selector = encodeFnSelector('setGreenlistEnable(bool)'); + const masterRole = roles.common.greenlistedOperator; + const masterDelay = 7200; + const operatorDelay = 3600; + const permissionAccount = regularAccounts[0].address; + + await wAccessControlTester.setContractAdminRole(masterRole); + + const operatorRoleKey = await accessControl.grantOperatorRoleKey( + masterRole, + wAccessControlTester.address, + selector, + ); + + await setGrantOperatorRoleTester( + { accessControl, owner }, + wAccessControlTester.address, + [ + { + functionSelector: selector, + operator: owner.address, + enabled: true, + }, + ], + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [masterRole, operatorRoleKey], + [masterDelay, operatorDelay], + ); + + const data = encodeSetPermissionRoleMult( + accessControl, + wAccessControlTester.address, + selector, + permissionAccount, + ); + + const [roleUsed] = await timelockManager.getTargetRole( + accessControl.address, + data, + owner.address, + ); + expect(roleUsed).eq(operatorRoleKey); + + await scheduleAndExecuteSetPermissionRoleMult( + { accessControl, owner, timelock, timelockManager }, + data, + operatorDelay, + owner, + ); + + expect( + await accessControl[ + 'hasFunctionPermission(bytes32,address,bytes4,address)' + ]( + masterRole, + wAccessControlTester.address, + selector, + permissionAccount, + ), + ).eq(true); + }); + }); + }); + + describe('grantRole()', () => { + it('should fail: when sender does not have role admin role', async () => { + const { accessControl, regularAccounts, roles } = await loadFixture( + defaultDeploy, + ); + + await grantRoleTester( + { accessControl, owner: regularAccounts[0] }, + roles.common.blacklisted, + regularAccounts[1].address, + undefined, + { revertCustomError: acErrors.WMAC_HASNT_PERMISSION() }, + ); + }); + + it('when timelock delay is not 0 - schedule and execute the tx', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + timelock, + timelockManager, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [roles.common.blacklistedOperator], + [3600], + ); + + const data = accessControl.interface.encodeFunctionData( + 'grantRole(bytes32,address)', + [roles.common.blacklisted, regularAccounts[0].address], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [data], + {}, + { from: owner }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + data, + owner.address, + { from: owner }, + ); + }); + + it('when role already granted - should not fail', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + await grantRoleTester( + { accessControl, owner }, + roles.common.blacklisted, + regularAccounts[0].address, + ); + + await grantRoleTester( + { accessControl, owner }, + roles.common.blacklisted, + regularAccounts[0].address, + ); + }); + + it('when delay is not NULL_DELAY but actual delay is NULL_DELAY - should set the delay', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + await grantRoleTester( + { accessControl, owner }, + roles.common.blacklisted, + regularAccounts[0].address, + 3600, + ); + }); + + it('should fail: when delay is not NULL_DELAY but actual delay is also not null', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + await grantRoleTester( + { accessControl, owner }, + roles.common.blacklisted, + regularAccounts[0].address, + 3600, + ); + + await grantRoleTester( + { accessControl, owner }, + roles.common.blacklisted, + regularAccounts[1].address, + 7200, + { revertCustomError: { customErrorName: 'DelayIsAlreadySet' } }, + ); + }); + }); + + describe('revokeRole()', () => { + it('should fail: when sender does not have role admin role', async () => { + const { accessControl, regularAccounts, roles } = await loadFixture( + defaultDeploy, + ); + + await revokeRoleTester( + { accessControl, owner: regularAccounts[0] }, + roles.common.blacklisted, + regularAccounts[1].address, + { revertCustomError: acErrors.WMAC_HASNT_PERMISSION() }, + ); + }); + + it('when timelock delay is not 0 - schedule and execute the tx', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + timelock, + timelockManager, + } = await loadFixture(defaultDeploy); + + await grantRoleTester( + { accessControl, owner }, + roles.common.blacklisted, + regularAccounts[0].address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [roles.common.blacklistedOperator], + [3600], + ); + + const data = accessControl.interface.encodeFunctionData('revokeRole', [ + roles.common.blacklisted, + regularAccounts[0].address, + ]); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [data], + {}, + { from: owner }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + data, + owner.address, + { from: owner }, + ); + }); + + it('when role already revoked - should not fail', async () => { + const { accessControl, owner, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + await revokeRoleTester( + { accessControl, owner }, + roles.common.blacklisted, + regularAccounts[0].address, + ); + }); + + it('should fail: when revoking DEFAULT_ADMIN_ROLE from self', async () => { + const { accessControl, owner, roles } = await loadFixture(defaultDeploy); + + await revokeRoleTester( + { accessControl, owner }, + roles.common.defaultAdmin, + owner.address, + { revertCustomError: { customErrorName: 'CannotRevokeFromSelf' } }, + ); + }); + + it('when revoking role from self but its not DEFAULT_ADMIN_ROLE (should not fail)', async () => { + const { accessControl, owner, roles } = await loadFixture(defaultDeploy); + + await revokeRoleTester( + { accessControl, owner }, + roles.common.blacklistedOperator, + owner.address, + ); + }); + + it('should fail: when revoking DEFAULT_ADMIN_ROLE from self and timelock delay is not 0', async () => { + // Locks in that _validateRevokeRole uses the resolved proposer (actualSender), + // not msg.sender. If it compared against the timelock address, self-revoke + // via timelock would incorrectly succeed. + const { accessControl, owner, roles, timelock, timelockManager } = + await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [roles.common.defaultAdmin], + [3600], + ); + + const data = accessControl.interface.encodeFunctionData('revokeRole', [ + roles.common.defaultAdmin, + owner.address, + ]); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [data], + {}, + { from: owner }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + data, + owner.address, + { + from: owner, + revertMessage: 'TimelockController: underlying transaction reverted', + }, + ); + + expect( + await accessControl.hasRole(roles.common.defaultAdmin, owner.address), + ).eq(true); + }); + + it('when revoking DEFAULT_ADMIN_ROLE from another admin via timelock - succeeds', async () => { + const { + accessControl, + owner, + regularAccounts, + roles, + timelock, + timelockManager, + } = await loadFixture(defaultDeploy); + + const otherAdmin = regularAccounts[0]; + + await grantRoleTester( + { accessControl, owner }, + roles.common.defaultAdmin, + otherAdmin.address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [roles.common.defaultAdmin], + [3600], + ); + + const data = accessControl.interface.encodeFunctionData('revokeRole', [ + roles.common.defaultAdmin, + otherAdmin.address, + ]); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [data], + {}, + { from: owner }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + data, + owner.address, + { from: owner }, + ); + + expect( + await accessControl.hasRole( + roles.common.defaultAdmin, + otherAdmin.address, + ), + ).eq(false); + expect( + await accessControl.hasRole(roles.common.defaultAdmin, owner.address), + ).eq(true); + }); + }); + + describe('setRoleDelayMult()', () => { + it('should set role delays', async () => { + const { timelockManager, timelock, owner, accessControl } = + await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [7200], + ); + }); + + it('should fail: when params array is empty', async () => { + const { timelockManager, timelock, owner, accessControl } = + await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [], + [], + { + revertCustomError: { + customErrorName: 'EmptyArray', + }, + }, + ); + }); + + it('should fail: when caller do not have DEFAULT_ADMIN_ROLE', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION( + undefined, + timelockManager, + ), + from: regularAccounts[0], + }, + ); + }); + }); + + describe('setDefaultDelay()', () => { + it('should require 2 days timelock, even if role timelock is different', async () => { + const { timelockManager, timelock, owner, accessControl, roles } = + await loadFixture(defaultDeploy); + + const defaultAdminRole = roles.common.defaultAdmin; + const newDelay = 7200; + + await setRoleTimelocksTester( + { owner, accessControl, timelockManager, timelock }, + [defaultAdminRole], + [3600], + ); + + const calldata = accessControl.interface.encodeFunctionData( + 'setDefaultDelay', + [newDelay], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [calldata], + ); + await increase(DELAY_FOR_SET_DEFAULT_DELAY); + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + calldata, + owner.address, + ); + }); + + it('should fail: when called from a wallet without default admin role', async () => { + const { + accessControl, + owner, + timelock, + timelockManager, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await setDefaultDelayTest( + { owner, accessControl, timelock, timelockManager }, + 7200, + { + revertCustomError: { + customErrorName: 'NoFunctionPermission', + }, + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when called from a function admin role', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + regularAccounts, + roles, + } = await loadFixture(defaultDeploy); + + const defaultAdminRole = roles.common.defaultAdmin; + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: defaultAdminRole, + targetContract: accessControl.address, + functionSelector: setDefaultDelaySelector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + accessControl.address, + setDefaultDelaySelector, + [{ account: regularAccounts[0].address, enabled: true }], + ); + + await setDefaultDelayTest( + { timelockManager, timelock, owner, accessControl }, + 7200, + { + revertCustomError: { + customErrorName: 'NoFunctionPermission', + }, + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when called directly without timelock', async () => { + const { timelockManager, timelock, owner, accessControl, roles } = + await loadFixture(defaultDeploy); + + await setDefaultDelayTest( + { timelockManager, timelock, owner, accessControl }, + 7200, + { + revertCustomError: { + contract: accessControl, + customErrorName: 'SenderIsNotTimelock', + }, + }, + ); + }); + }); + + describe('defaultDelay()', () => { + it('should return default timelock delay', async () => { + const { accessControl } = await loadFixture(defaultDeploy); + + await accessControl.setDefaultDelayTest(3600); + + expect(await accessControl.defaultDelay()).to.eq(3600); + }); + }); + + describe('getRoleTimelockDelay()', () => { + it('should return default delay when role delay is not set', async () => { + const { accessControl } = await loadFixture(defaultDeploy); + + await accessControl.setDefaultDelayTest(3600); + + const [delay, isDefault] = await accessControl.getRoleTimelockDelay( + constants.HashZero, + 0, + ); + + expect(delay).to.eq(3600); + expect(isDefault).to.eq(true); + }); + + it('should return default delay when override delay is 0 and role delay is not set', async () => { + const { accessControl } = await loadFixture(defaultDeploy); + + await accessControl.setDefaultDelayTest(3600); + + const [delay, isDefault] = await accessControl.getRoleTimelockDelay( + constants.HashZero, + 0, + ); + + expect(delay).to.eq(3600); + expect(isDefault).to.eq(true); + }); + + it('should return override delay if its not zero if role delay is not set', async () => { + const { accessControl } = await loadFixture(defaultDeploy); + + await accessControl.setDefaultDelayTest(3600); + + const [delay, isDefault] = await accessControl.getRoleTimelockDelay( + constants.HashZero, + 1, + ); + + expect(delay).to.eq(1); + expect(isDefault).to.eq(false); + }); + + it('should return override delay if its not zero if role delay is set', async () => { + const { timelockManager, timelock, owner, accessControl } = + await loadFixture(defaultDeploy); + + await accessControl.setDefaultDelayTest(3600); + + await setRoleTimelocksAndExecute( + { timelockManager, timelock, owner, accessControl }, + [{ role: constants.HashZero, delay: 1 }], + ); + + const [delay, isDefault] = await accessControl.getRoleTimelockDelay( + constants.HashZero, + 2, + ); + + expect(delay).to.eq(2); + expect(isDefault).to.eq(false); + }); + + it('should return 0 if override delay is NO_DELAY (uint256.max)', async () => { + const { accessControl } = await loadFixture(defaultDeploy); + + await accessControl.setDefaultDelayTest(3600); + + const [delay, isDefault] = await accessControl.getRoleTimelockDelay( + constants.HashZero, + NO_DELAY, + ); + + expect(delay).to.eq(0); + expect(isDefault).to.eq(false); + }); + + it('should return configured delay when role delay is set', async () => { + const { timelockManager, timelock, owner, accessControl } = + await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [7200], + ); + + const [delay, isDefault] = await accessControl.getRoleTimelockDelay( + constants.HashZero, + 0, + ); + + expect(delay).to.eq(7200); + expect(isDefault).to.eq(false); + }); + + it('should return zero delay when role delay is max uint256', async () => { + const { timelockManager, timelock, owner, accessControl } = + await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [NO_DELAY], + ); + + const [delay, isDefault] = await accessControl.getRoleTimelockDelay( + constants.HashZero, + 0, + ); + + expect(delay).to.eq(0); + expect(isDefault).to.eq(false); + }); + }); +}); + +describe('WithMidasAccessControl', function () { + it('deployment', async () => { + const { accessControl, wAccessControlTester } = await loadFixture( + defaultDeploy, + ); + expect(await wAccessControlTester.accessControl()).eq( + accessControl.address, + ); + }); + + it('onlyInitializing', async () => { + const { accessControl, owner } = await loadFixture(defaultDeploy); + + const wac = await new WithMidasAccessControlTester__factory(owner).deploy(); + + await expect( + wac.initializeWithoutInitializer(accessControl.address), + ).revertedWith('Initializable: contract is not initializing'); + }); + + describe('modifier onlyRole', () => { + it('should fail: when call from address without role', async () => { + const { wAccessControlTester, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + await expect( + wAccessControlTester + .connect(regularAccounts[1]) + .withOnlyRole(roles.common.defaultAdmin, false), + ).revertedWithCustomError( + wAccessControlTester, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + + it('when validateFunctionRole is false and caller has root admin role', async () => { + const { wAccessControlTester, roles } = await loadFixture(defaultDeploy); + + await expect( + wAccessControlTester.withOnlyRole(roles.common.defaultAdmin, false), + ).not.reverted; + }); + + it('should fail: when role is timelocked and trying to call the function directly', async () => { + const { + accessControl, + wAccessControlTester, + owner, + regularAccounts, + roles, + timelockManager, + timelock, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + + const { wacFunctionKey, timelockManagerFunctionKey } = + await setupWithOnlyRolePermission( + accessControl, + owner, + wAccessControlTester, + timelockManager, + adminRole, + regularAccounts[0].address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [wacFunctionKey, timelockManagerFunctionKey], + [3600, 3600], + ); + + await expect( + wAccessControlTester + .connect(regularAccounts[0]) + .withOnlyRole(adminRole, true), + ).revertedWithCustomError(accessControl, 'SenderIsNotTimelock'); + }); + + it('should fail: when validateFunctionRole is false but trying to call with function admin', async () => { + const { + accessControl, + wAccessControlTester, + owner, + regularAccounts, + roles, + timelockManager, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + + await setupWithOnlyRolePermission( + accessControl, + owner, + wAccessControlTester, + timelockManager, + adminRole, + regularAccounts[0].address, + ); + + await expect( + wAccessControlTester + .connect(regularAccounts[0]) + .withOnlyRole(adminRole, false), + ).revertedWithCustomError( + wAccessControlTester, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + + it('when validateFunctionRole is true and trying to call with function admin and there is no timelock', async () => { + const { + accessControl, + wAccessControlTester, + owner, + regularAccounts, + roles, + timelockManager, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + + await setupWithOnlyRolePermission( + accessControl, + owner, + wAccessControlTester, + timelockManager, + adminRole, + regularAccounts[0].address, + ); + + await expect( + wAccessControlTester + .connect(regularAccounts[0]) + .withOnlyRole(adminRole, true), + ).not.reverted; + }); + + it('when validateFunctionRole is true and trying to call with function admin and there is timelock on function role', async () => { + const { + accessControl, + wAccessControlTester, + owner, + regularAccounts, + roles, + timelockManager, + timelock, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + + const { wacFunctionKey, timelockManagerFunctionKey } = + await setupWithOnlyRolePermission( + accessControl, + owner, + wAccessControlTester, + timelockManager, + adminRole, + regularAccounts[0].address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [wacFunctionKey, timelockManagerFunctionKey], + [3600, 3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyRole', + [adminRole, true], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + {}, + { from: regularAccounts[0] }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + regularAccounts[0].address, + ); + }); + + it('when validateFunctionRole is true and trying to call with function admin and there is timelock on root role', async () => { + const { + accessControl, + wAccessControlTester, + owner, + regularAccounts, + roles, + timelockManager, + timelock, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + + await grantRoleTester( + { accessControl, owner }, + adminRole, + regularAccounts[0].address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [adminRole], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyRole', + [adminRole, true], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + {}, + { from: regularAccounts[0] }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + regularAccounts[0].address, + ); + }); + + it('when validateFunctionRole is true, caller has both function admin and root roles, delay is on both - it should select role with lower delay', async () => { + const { + accessControl, + wAccessControlTester, + owner, + regularAccounts, + roles, + timelockManager, + timelock, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + + await grantRoleTester( + { accessControl, owner }, + adminRole, + regularAccounts[0].address, + ); + + const { wacFunctionKey, timelockManagerFunctionKey } = + await setupWithOnlyRolePermission( + accessControl, + owner, + wAccessControlTester, + timelockManager, + adminRole, + regularAccounts[0].address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [adminRole, wacFunctionKey, timelockManagerFunctionKey], + [7200, 3600, 3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyRole', + [adminRole, true], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + {}, + { from: regularAccounts[0] }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + regularAccounts[0].address, + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + {}, + { from: regularAccounts[0] }, + ); + + await increase(1); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + regularAccounts[0].address, + timelockManagerRevertOpts(timelockManager, 'TimelockOperationNotReady'), + ); + + await increase(3599); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + regularAccounts[0].address, + ); + }); + }); + + describe('modifier onlyContractAdmin', () => { + const setupContractAdminRole = async ( + wAccessControlTester: WithMidasAccessControlTester, + adminRole: string, + ) => { + await wAccessControlTester.setContractAdminRole(adminRole); + }; + + it('should fail: when call from address without role', async () => { + const { wAccessControlTester, regularAccounts, roles } = + await loadFixture(defaultDeploy); + + await setupContractAdminRole( + wAccessControlTester, + roles.common.defaultAdmin, + ); + + await expect( + wAccessControlTester + .connect(regularAccounts[1]) + .withOnlyContractAdmin(), + ).revertedWithCustomError( + wAccessControlTester, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + + it('when caller has root admin role', async () => { + const { wAccessControlTester, roles } = await loadFixture(defaultDeploy); + + await setupContractAdminRole( + wAccessControlTester, + roles.common.defaultAdmin, + ); + + await expect(wAccessControlTester.withOnlyContractAdmin()).not.reverted; + }); + + it('should fail: when role is timelocked and trying to call the function directly', async () => { + const { + accessControl, + wAccessControlTester, + owner, + regularAccounts, + roles, + timelockManager, + timelock, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + await setupContractAdminRole(wAccessControlTester, adminRole); + + const { wacFunctionKey, timelockManagerFunctionKey } = + await setupWithOnlyContractAdminPermission( + accessControl, + owner, + wAccessControlTester, + timelockManager, + adminRole, + regularAccounts[0].address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [wacFunctionKey, timelockManagerFunctionKey], + [3600, 3600], + ); + + await expect( + wAccessControlTester + .connect(regularAccounts[0]) + .withOnlyContractAdmin(), + ).revertedWithCustomError(accessControl, 'SenderIsNotTimelock'); + }); + + it('when trying to call with function admin and there is no timelock', async () => { + const { + accessControl, + wAccessControlTester, + owner, + regularAccounts, + roles, + timelockManager, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + await setupContractAdminRole(wAccessControlTester, adminRole); + + await setupWithOnlyContractAdminPermission( + accessControl, + owner, + wAccessControlTester, + timelockManager, + adminRole, + regularAccounts[0].address, + ); + + await expect( + wAccessControlTester + .connect(regularAccounts[0]) + .withOnlyContractAdmin(), + ).not.reverted; + }); + + it('when trying to call with function admin and there is timelock on function role', async () => { + const { + accessControl, + wAccessControlTester, + owner, + regularAccounts, + roles, + timelockManager, + timelock, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + await setupContractAdminRole(wAccessControlTester, adminRole); + + const { wacFunctionKey, timelockManagerFunctionKey } = + await setupWithOnlyContractAdminPermission( + accessControl, + owner, + wAccessControlTester, + timelockManager, + adminRole, + regularAccounts[0].address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [wacFunctionKey, timelockManagerFunctionKey], + [3600, 3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + {}, + { from: regularAccounts[0] }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + regularAccounts[0].address, + ); + }); + + it('when trying to call with function admin and there is timelock on root role', async () => { + const { + accessControl, + wAccessControlTester, + owner, + regularAccounts, + roles, + timelockManager, + timelock, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + await setupContractAdminRole(wAccessControlTester, adminRole); + + await grantRoleTester( + { accessControl, owner }, + adminRole, + regularAccounts[0].address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [adminRole], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + {}, + { from: regularAccounts[0] }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + regularAccounts[0].address, + ); + }); + + it('when caller has both function admin and root roles, delay is on both - it should select role with lower delay', async () => { + const { + accessControl, + wAccessControlTester, + owner, + regularAccounts, + roles, + timelockManager, + timelock, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + await setupContractAdminRole(wAccessControlTester, adminRole); + + await grantRoleTester( + { accessControl, owner }, + adminRole, + regularAccounts[0].address, + ); + + const { wacFunctionKey, timelockManagerFunctionKey } = + await setupWithOnlyContractAdminPermission( + accessControl, + owner, + wAccessControlTester, + timelockManager, + adminRole, + regularAccounts[0].address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [adminRole, wacFunctionKey, timelockManagerFunctionKey], + [7200, 3600, 3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + {}, + { from: regularAccounts[0] }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + regularAccounts[0].address, + ); + }); + }); + + describe('modifier onlyRoleNoTimelock', () => { + it('should fail: when trying to schedule through timelock', async () => { + const { + wAccessControlTester, + owner, + roles, + timelockManager, + timelock, + accessControl, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyRoleNoTimelock', + [adminRole, true], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + {}, + timelockManagerRevertOpts(timelockManager, 'InvalidPreflightError'), + ); + }); + + it('when validateFunctionRole is true and user has only root admin role', async () => { + const { wAccessControlTester, roles } = await loadFixture(defaultDeploy); + + await expect( + wAccessControlTester.withOnlyRoleNoTimelock( + roles.common.defaultAdmin, + true, + ), + ).not.reverted; + }); + + it('when validateFunctionRole is false and user has only root admin role', async () => { + const { wAccessControlTester, roles } = await loadFixture(defaultDeploy); + + await expect( + wAccessControlTester.withOnlyRoleNoTimelock( + roles.common.defaultAdmin, + false, + ), + ).not.reverted; + }); + + it('when validateFunctionRole is true and user has only function admin role', async () => { + const { + accessControl, + wAccessControlTester, + owner, + regularAccounts, + roles, + timelockManager, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + + await setupFunctionPermissionRole( + accessControl, + owner, + wAccessControlTester, + timelockManager, + adminRole, + withOnlyRoleNoTimelockSelector, + regularAccounts[0].address, + ); + + await expect( + wAccessControlTester + .connect(regularAccounts[0]) + .withOnlyRoleNoTimelock(adminRole, true), + ).not.reverted; + }); + + it('should fail: when validateFunctionRole is false and user has only function admin role', async () => { + const { + accessControl, + wAccessControlTester, + owner, + regularAccounts, + roles, + timelockManager, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + + await setupFunctionPermissionRole( + accessControl, + owner, + wAccessControlTester, + timelockManager, + adminRole, + withOnlyRoleNoTimelockSelector, + regularAccounts[0].address, + ); + + await expect( + wAccessControlTester + .connect(regularAccounts[0]) + .withOnlyRoleNoTimelock(adminRole, false), + ).revertedWithCustomError( + wAccessControlTester, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + + it('when user has both roles', async () => { + const { + accessControl, + wAccessControlTester, + owner, + regularAccounts, + roles, + timelockManager, + } = await loadFixture(defaultDeploy); + + const adminRole = roles.common.defaultAdmin; + + await grantRoleTester( + { accessControl, owner }, + adminRole, + regularAccounts[0].address, + ); + + await setupFunctionPermissionRole( + accessControl, + owner, + wAccessControlTester, + timelockManager, + adminRole, + withOnlyRoleNoTimelockSelector, + regularAccounts[0].address, + ); + + await expect( + wAccessControlTester + .connect(regularAccounts[0]) + .withOnlyRoleNoTimelock(adminRole, true), + ).not.reverted; }); }); }); diff --git a/test/unit/MidasInitializable.test.ts b/test/unit/MidasInitializable.test.ts new file mode 100644 index 00000000..248b8dab --- /dev/null +++ b/test/unit/MidasInitializable.test.ts @@ -0,0 +1,80 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +import { MidasInitializableTester } from '../../typechain-types'; +import { deployProxyContract } from '../common/deploy.helpers'; + +const PROXY_ADMIN_SLOT = + '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103'; + +const INITIALIZED_SLOT = ethers.utils.hexZeroPad('0x00', 32); + +const setProxyAdmin = (address: string, admin: string) => + ethers.provider.send('hardhat_setStorageAt', [ + address, + PROXY_ADMIN_SLOT, + ethers.utils.hexZeroPad(admin, 32), + ]); + +const setInitializedVersion = (address: string, version: number) => + ethers.provider.send('hardhat_setStorageAt', [ + address, + INITIALIZED_SLOT, + ethers.utils.hexZeroPad(ethers.utils.hexlify(version), 32), + ]); + +const deployTester = () => + deployProxyContract( + 'MidasInitializableTester', + [], + 'initialize', + ); + +describe('MidasInitializable', function () { + it('fresh deploy: initialize runs initializeV2 while proxy admin is zero', async () => { + const tester = await deployTester(); + + expect(await tester.initializeCallsCount()).eq(1); + expect(await tester.reinitCallsCount()).eq(1); + }); + + describe('initializeV2()', () => { + it('should fail: when already reinitialized, even from the proxy admin', async () => { + const [, admin, stranger] = await ethers.getSigners(); + const tester = await deployTester(); + + await setProxyAdmin(tester.address, admin.address); + + await expect(tester.connect(admin).initializeV2()).revertedWith( + 'Initializable: contract is already initialized', + ); + await expect(tester.connect(stranger).initializeV2()).revertedWith( + 'Initializable: contract is already initialized', + ); + }); + + it('should fail: when initialized and caller is not the proxy admin', async () => { + const [, admin, stranger] = await ethers.getSigners(); + const tester = await deployTester(); + + await setProxyAdmin(tester.address, admin.address); + await setInitializedVersion(tester.address, 1); + + await expect( + tester.connect(stranger).initializeV2(), + ).revertedWithCustomError(tester, 'SenderNotProxyAdmin'); + }); + + it('when initialized and caller is the proxy admin', async () => { + const [, admin] = await ethers.getSigners(); + const tester = await deployTester(); + + await setProxyAdmin(tester.address, admin.address); + await setInitializedVersion(tester.address, 1); + + await tester.connect(admin).initializeV2(); + + expect(await tester.reinitCallsCount()).eq(2); + }); + }); +}); diff --git a/test/unit/MidasPauseManager.test.ts b/test/unit/MidasPauseManager.test.ts new file mode 100644 index 00000000..1e1c70a7 --- /dev/null +++ b/test/unit/MidasPauseManager.test.ts @@ -0,0 +1,1881 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { increase } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { BigNumberish, Contract } from 'ethers'; + +import { encodeFnSelector } from '../../helpers/utils'; +import { + MidasAccessControl, + MidasAccessControlTimelockController, + MidasPauseManager, +} from '../../typechain-types'; +import { + acErrors, + NO_DELAY, + NULL_DELAY, + setPermissionRoleTester, + setRoleTimelocksTester, + setupGrantOperatorRole, +} from '../common/ac.helpers'; +import { + adminPauseContractTest, + adminUnpauseContractTest, + pauseGlobalTest, + pauseVault, + pauseVaultFn, + unpauseGlobalTest, + unpauseVault, + unpauseVaultFn, +} from '../common/common.helpers'; +import { defaultDeploy } from '../common/fixtures'; +import { + executeTimelockOperationTester, + bulkScheduleTimelockOperationTester, +} from '../common/timelock-manager.helpers'; + +const DEFAULT_UNPAUSE_DELAY = 86400; +const DELAY_FOR_SET_DELAY = 2 * 24 * 3600; +const ROLE_TIMELOCK_DELAY = 3600; +const CUSTOM_DELAY = 3600; + +type TimelockCtx = { + pauseManager: MidasPauseManager; + timelockManager: Awaited>['timelockManager']; + timelock: MidasAccessControlTimelockController; + accessControl: MidasAccessControl; + owner: SignerWithAddress; +}; + +const timelockParams = (ctx: TimelockCtx) => ({ + timelockManager: ctx.timelockManager, + timelock: ctx.timelock, + accessControl: ctx.accessControl, + owner: ctx.owner, +}); + +const scheduleAndExecute = async ( + ctx: TimelockCtx, + calldata: string, + delay: number, + from: SignerWithAddress = ctx.owner, + executeOpt?: Parameters[4], +) => { + const timelockCtx = timelockParams(ctx); + + await bulkScheduleTimelockOperationTester( + timelockCtx, + [ctx.pauseManager.address], + [calldata], + {}, + { from }, + ); + await increase(delay); + await executeTimelockOperationTester( + timelockCtx, + ctx.pauseManager.address, + calldata, + from.address, + executeOpt, + ); +}; + +const unpauseContractsViaTimelock = async ( + ctx: TimelockCtx, + addresses: string[], + from: SignerWithAddress = ctx.owner, +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'bulkUnpauseContract', + [addresses], + ); + await scheduleAndExecute(ctx, calldata, DEFAULT_UNPAUSE_DELAY, from); +}; + +const unpauseGlobalViaTimelock = async ( + ctx: TimelockCtx, + from: SignerWithAddress = ctx.owner, +) => { + const calldata = + ctx.pauseManager.interface.encodeFunctionData('globalUnpause'); + await scheduleAndExecute(ctx, calldata, DEFAULT_UNPAUSE_DELAY, from); +}; + +const unpauseContractFnsViaTimelock = async ( + ctx: TimelockCtx, + addresses: string[], + selectors: string[], + from: SignerWithAddress = ctx.owner, +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'bulkUnpauseContractFn', + [addresses, selectors], + ); + await scheduleAndExecute(ctx, calldata, DEFAULT_UNPAUSE_DELAY, from); +}; + +const contractAdminUnpauseViaTimelock = async ( + ctx: TimelockCtx, + contract: Contract, + from: SignerWithAddress = ctx.owner, +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'contractAdminUnpause', + [contract.address], + ); + await scheduleAndExecute(ctx, calldata, DEFAULT_UNPAUSE_DELAY, from); +}; + +const setPauseDelayViaTimelock = async ( + ctx: TimelockCtx, + newDelay: number, + from: SignerWithAddress = ctx.owner, +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'setPauseDelay', + [newDelay], + ); + await scheduleAndExecute(ctx, calldata, DELAY_FOR_SET_DELAY, from); +}; + +const setUnpauseDelayViaTimelock = async ( + ctx: TimelockCtx, + newDelay: BigNumberish, + from: SignerWithAddress = ctx.owner, +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'setUnpauseDelay', + [newDelay], + ); + await scheduleAndExecute(ctx, calldata, DELAY_FOR_SET_DELAY, from); +}; + +const grantContractPauser = async ( + accessControl: MidasAccessControl, + pausableTester: Contract, + account: SignerWithAddress, +) => { + const role = await pausableTester.contractAdminRole(); + await accessControl['grantRole(bytes32,address)'](role, account.address); +}; + +const toTimelockCtx = ( + fixture: Awaited>, +): TimelockCtx => ({ + pauseManager: fixture.pauseManager, + timelockManager: fixture.timelockManager, + timelock: fixture.timelock, + accessControl: fixture.accessControl, + owner: fixture.owner, +}); + +const scheduleGlobalPause = async ( + ctx: TimelockCtx, + opt?: Parameters[4], +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData('globalPause'); + await bulkScheduleTimelockOperationTester( + timelockParams(ctx), + [ctx.pauseManager.address], + [calldata], + {}, + opt, + ); +}; + +const scheduleGlobalUnpause = async ( + ctx: TimelockCtx, + opt?: Parameters[4], +) => { + const calldata = + ctx.pauseManager.interface.encodeFunctionData('globalUnpause'); + await bulkScheduleTimelockOperationTester( + timelockParams(ctx), + [ctx.pauseManager.address], + [calldata], + {}, + opt, + ); +}; + +const executeGlobalPause = async ( + ctx: TimelockCtx, + opt?: Parameters[4], +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData('globalPause'); + await executeTimelockOperationTester( + timelockParams(ctx), + ctx.pauseManager.address, + calldata, + ctx.owner.address, + opt, + ); +}; + +const executeGlobalUnpause = async ( + ctx: TimelockCtx, + opt?: Parameters[4], +) => { + const calldata = + ctx.pauseManager.interface.encodeFunctionData('globalUnpause'); + await executeTimelockOperationTester( + timelockParams(ctx), + ctx.pauseManager.address, + calldata, + ctx.owner.address, + opt, + ); +}; + +const setPauseAdminRoleTimelock = async ( + fixture: Awaited>, + delay: number = ROLE_TIMELOCK_DELAY, +) => { + const pauseAdminRole = await fixture.pauseManager.pauseAdminRole(); + await setRoleTimelocksTester( + { + timelockManager: fixture.timelockManager, + timelock: fixture.timelock, + owner: fixture.owner, + accessControl: fixture.accessControl, + }, + [pauseAdminRole], + [delay], + ); +}; + +const setContractPauserRoleTimelock = async ( + fixture: Awaited>, + delay: number = ROLE_TIMELOCK_DELAY, +) => { + const contractPauserRole = await fixture.pausableTester.contractAdminRole(); + await setRoleTimelocksTester( + { + timelockManager: fixture.timelockManager, + timelock: fixture.timelock, + owner: fixture.owner, + accessControl: fixture.accessControl, + }, + [contractPauserRole], + [delay], + ); +}; + +const BULK_PAUSE_CONTRACT_SEL = encodeFnSelector( + 'bulkPauseContract(address[])', +); +const BULK_UNPAUSE_CONTRACT_SEL = encodeFnSelector( + 'bulkUnpauseContract(address[])', +); +const BULK_PAUSE_CONTRACT_FN_SEL = encodeFnSelector( + 'bulkPauseContractFn(address[],bytes4[])', +); +const BULK_UNPAUSE_CONTRACT_FN_SEL = encodeFnSelector( + 'bulkUnpauseContractFn(address[],bytes4[])', +); +const CONTRACT_ADMIN_PAUSE_SEL = encodeFnSelector( + 'contractAdminPause(address)', +); +const CONTRACT_ADMIN_UNPAUSE_SEL = encodeFnSelector( + 'contractAdminUnpause(address)', +); +const DEPOSIT_REQUEST_SEL = encodeFnSelector( + 'depositRequest(address,uint256,bytes32)', +); + +const noTimelockDelayRevert = ( + fixture: Awaited>, +) => ({ + revertCustomError: { + contract: fixture.timelockManager, + customErrorName: 'NoTimelockDelayForRole', + }, +}); + +const pauseAdminFunctionNotReady = async ( + fixture: Awaited>, + selector: string, + owner?: SignerWithAddress, +) => ({ + revertCustomError: { + contract: fixture.accessControl, + customErrorName: 'SenderIsNotTimelock', + args: [ + await fixture.pauseManager.pauseAdminRole(), + selector, + owner?.address ?? fixture.owner.address, + ], + }, +}); + +const contractPauserFunctionNotReady = async ( + fixture: Awaited>, + selector: string, + owner?: SignerWithAddress, +) => ({ + revertCustomError: { + contract: fixture.accessControl, + customErrorName: 'SenderIsNotTimelock', + args: [ + await fixture.pausableTester.contractAdminRole(), + selector, + owner?.address ?? fixture.owner.address, + ], + }, +}); + +const scheduleBulkPauseContract = async ( + ctx: TimelockCtx, + addresses: string[], + opt?: Parameters[4], +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'bulkPauseContract', + [addresses], + ); + await bulkScheduleTimelockOperationTester( + timelockParams(ctx), + [ctx.pauseManager.address], + [calldata], + {}, + opt, + ); +}; + +const executeBulkPauseContract = async ( + ctx: TimelockCtx, + addresses: string[], + opt?: Parameters[4], +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'bulkPauseContract', + [addresses], + ); + await executeTimelockOperationTester( + timelockParams(ctx), + ctx.pauseManager.address, + calldata, + ctx.owner.address, + opt, + ); +}; + +const scheduleBulkUnpauseContract = async ( + ctx: TimelockCtx, + addresses: string[], + opt?: Parameters[4], +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'bulkUnpauseContract', + [addresses], + ); + await bulkScheduleTimelockOperationTester( + timelockParams(ctx), + [ctx.pauseManager.address], + [calldata], + {}, + opt, + ); +}; + +const executeBulkUnpauseContract = async ( + ctx: TimelockCtx, + addresses: string[], + opt?: Parameters[4], +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'bulkUnpauseContract', + [addresses], + ); + await executeTimelockOperationTester( + timelockParams(ctx), + ctx.pauseManager.address, + calldata, + ctx.owner.address, + opt, + ); +}; + +const scheduleBulkPauseContractFn = async ( + ctx: TimelockCtx, + addresses: string[], + selectors: string[], + opt?: Parameters[4], +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'bulkPauseContractFn', + [addresses, selectors], + ); + await bulkScheduleTimelockOperationTester( + timelockParams(ctx), + [ctx.pauseManager.address], + [calldata], + {}, + opt, + ); +}; + +const executeBulkPauseContractFn = async ( + ctx: TimelockCtx, + addresses: string[], + selectors: string[], + opt?: Parameters[4], +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'bulkPauseContractFn', + [addresses, selectors], + ); + await executeTimelockOperationTester( + timelockParams(ctx), + ctx.pauseManager.address, + calldata, + ctx.owner.address, + opt, + ); +}; + +const scheduleBulkUnpauseContractFn = async ( + ctx: TimelockCtx, + addresses: string[], + selectors: string[], + opt?: Parameters[4], +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'bulkUnpauseContractFn', + [addresses, selectors], + ); + await bulkScheduleTimelockOperationTester( + timelockParams(ctx), + [ctx.pauseManager.address], + [calldata], + {}, + opt, + ); +}; + +const executeBulkUnpauseContractFn = async ( + ctx: TimelockCtx, + addresses: string[], + selectors: string[], + opt?: Parameters[4], +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'bulkUnpauseContractFn', + [addresses, selectors], + ); + await executeTimelockOperationTester( + timelockParams(ctx), + ctx.pauseManager.address, + calldata, + ctx.owner.address, + opt, + ); +}; + +const scheduleContractAdminPause = async ( + ctx: TimelockCtx, + contractAddr: string, + opt?: Parameters[4], +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'contractAdminPause', + [contractAddr], + ); + await bulkScheduleTimelockOperationTester( + timelockParams(ctx), + [ctx.pauseManager.address], + [calldata], + {}, + opt, + ); +}; + +const executeContractAdminPause = async ( + ctx: TimelockCtx, + contractAddr: string, + opt?: Parameters[4], +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'contractAdminPause', + [contractAddr], + ); + await executeTimelockOperationTester( + timelockParams(ctx), + ctx.pauseManager.address, + calldata, + ctx.owner.address, + opt, + ); +}; + +const scheduleContractAdminUnpause = async ( + ctx: TimelockCtx, + contractAddr: string, + opt?: Parameters[4], +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'contractAdminUnpause', + [contractAddr], + ); + await bulkScheduleTimelockOperationTester( + timelockParams(ctx), + [ctx.pauseManager.address], + [calldata], + {}, + opt, + ); +}; + +const executeContractAdminUnpause = async ( + ctx: TimelockCtx, + contractAddr: string, + opt?: Parameters[4], +) => { + const calldata = ctx.pauseManager.interface.encodeFunctionData( + 'contractAdminUnpause', + [contractAddr], + ); + await executeTimelockOperationTester( + timelockParams(ctx), + ctx.pauseManager.address, + calldata, + ctx.owner.address, + opt, + ); +}; + +describe('MidasPauseManager', () => { + describe('globalPause()', () => { + it('should fail: when caller doesnt have pause admin role', async () => { + const { pauseManager, owner, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + await pauseGlobalTest( + { pauseManager, owner }, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('call from pause admin when already paused', async () => { + const { pauseManager, owner } = await loadFixture(defaultDeploy); + + await pauseGlobalTest({ pauseManager, owner }); + await pauseGlobalTest({ pauseManager, owner }); + }); + + it('call from pause admin', async () => { + const { pauseManager, owner } = await loadFixture(defaultDeploy); + + await pauseGlobalTest({ pauseManager, owner }); + }); + + it('when pause admin has function scoped timelock', async () => { + const { + accessControl, + pauseManager, + owner, + regularAccounts, + timelockManager, + timelock, + } = await loadFixture(defaultDeploy); + + const pauseAdminRole = await pauseManager.pauseAdminRole(); + const globalPauseSel = encodeFnSelector('globalPause()'); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: pauseAdminRole, + targetContract: pauseManager.address, + functionSelector: globalPauseSel, + grantOperator: owner, + }); + await setPermissionRoleTester( + { accessControl, owner }, + pauseAdminRole, + pauseManager.address, + globalPauseSel, + [{ account: regularAccounts[0].address, enabled: true }], + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [pauseAdminRole], + [3600], + ); + + await pauseGlobalTest( + { pauseManager, owner }, + { from: regularAccounts[0] }, + ); + }); + + it('should fail: when pause delay is no_delay and trying to schedule through timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await scheduleGlobalPause(ctx, { + revertCustomError: { + contract: fixture.timelockManager, + customErrorName: 'NoTimelockDelayForRole', + }, + }); + }); + + it('when pause delay is changed to custom delay, globalPause can be scheduled on timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setPauseDelayViaTimelock(ctx, CUSTOM_DELAY); + await scheduleGlobalPause(ctx); + await increase(CUSTOM_DELAY); + await executeGlobalPause(ctx); + }); + + it('should fail: when pause delay is custom delay and trying to call directly before delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setPauseDelayViaTimelock(ctx, CUSTOM_DELAY); + + await pauseGlobalTest(fixture, { + revertCustomError: { + contract: fixture.accessControl, + customErrorName: 'SenderIsNotTimelock', + args: [ + await fixture.pauseManager.pauseAdminRole(), + encodeFnSelector('globalPause()'), + fixture.owner.address, + ], + }, + }); + }); + + it('when pause delay is changed to null_delay, globalPause uses role timelock delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setPauseDelayViaTimelock(ctx, NULL_DELAY); + await setPauseAdminRoleTimelock(fixture); + await scheduleGlobalPause(ctx); + await increase(ROLE_TIMELOCK_DELAY); + await executeGlobalPause(ctx); + }); + }); + + describe('globalUnpause()', () => { + it('should fail: when caller doesnt have pause admin role', async () => { + const { pauseManager, regularAccounts, ...timelockFixture } = + await loadFixture(defaultDeploy); + + const calldata = + pauseManager.interface.encodeFunctionData('globalUnpause'); + + await bulkScheduleTimelockOperationTester( + timelockFixture, + [pauseManager.address], + [calldata], + {}, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('call from pause admin when not paused', async () => { + const fixture = await loadFixture(defaultDeploy); + await unpauseGlobalViaTimelock(fixture); + }); + + it('when unpause delay is set, unpause can be scheduled on timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + + await pauseGlobalTest(fixture); + await unpauseGlobalViaTimelock(fixture); + }); + + it('should fail: when trying to call directly before delay', async () => { + const { pauseManager, owner, accessControl } = await loadFixture( + defaultDeploy, + ); + + await pauseGlobalTest({ pauseManager, owner }); + + await unpauseGlobalTest( + { pauseManager, owner }, + { + revertCustomError: { + contract: accessControl, + customErrorName: 'SenderIsNotTimelock', + args: [ + await pauseManager.pauseAdminRole(), + encodeFnSelector('globalUnpause()'), + owner.address, + ], + }, + }, + ); + }); + + it('when unpause delay is changed to no_delay, globalUnpause can be called directly', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, NO_DELAY); + await pauseGlobalTest(fixture); + await unpauseGlobalTest(fixture); + }); + + it('should fail: when unpause delay is no_delay and trying to schedule through timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, NO_DELAY); + await pauseGlobalTest(fixture); + + await scheduleGlobalUnpause(ctx, { + revertCustomError: { + contract: fixture.timelockManager, + customErrorName: 'NoTimelockDelayForRole', + }, + }); + }); + + it('when unpause delay is changed to null_delay, globalUnpause uses role timelock delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, NULL_DELAY); + await setPauseAdminRoleTimelock(fixture); + await pauseGlobalTest(fixture); + await scheduleGlobalUnpause(ctx); + await increase(ROLE_TIMELOCK_DELAY); + await executeGlobalUnpause(ctx); + }); + + it('when unpause delay is changed to custom delay, globalUnpause can be scheduled on timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, CUSTOM_DELAY); + await pauseGlobalTest(fixture); + await scheduleGlobalUnpause(ctx); + await increase(CUSTOM_DELAY); + await executeGlobalUnpause(ctx); + }); + + it('should fail: when unpause delay is custom delay and trying to call directly before delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, CUSTOM_DELAY); + await pauseGlobalTest(fixture); + + await unpauseGlobalTest(fixture, { + revertCustomError: { + contract: fixture.accessControl, + customErrorName: 'SenderIsNotTimelock', + args: [ + await fixture.pauseManager.pauseAdminRole(), + encodeFnSelector('globalUnpause()'), + fixture.owner.address, + ], + }, + }); + }); + }); + + describe('bulkPauseContract()', () => { + it('should fail: when caller doesnt have pause admin role', async () => { + const { pausableTester, regularAccounts, pauseManager, owner } = + await loadFixture(defaultDeploy); + + await pauseVault({ pauseManager, owner }, pausableTester, { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }); + }); + + it('should fail: when caller has only contract pauser role', async () => { + const { + pausableTester, + regularAccounts, + pauseManager, + owner, + accessControl, + } = await loadFixture(defaultDeploy); + + await grantContractPauser( + accessControl, + pausableTester, + regularAccounts[0], + ); + + await pauseVault({ pauseManager, owner }, pausableTester, { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }); + }); + + it('call from pause admin when already paused', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( + defaultDeploy, + ); + + await pauseVault({ pauseManager, owner }, pausableTester); + await pauseVault({ pauseManager, owner }, pausableTester); + }); + + it('call from pause admin', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( + defaultDeploy, + ); + + await pauseVault({ pauseManager, owner }, pausableTester); + }); + + it('call from pause admin with multiple contracts', async () => { + const { pausableTester, depositVault, pauseManager, owner } = + await loadFixture(defaultDeploy); + + await pauseVault({ pauseManager, owner }, [pausableTester, depositVault]); + }); + + it('when pause admin has function scoped timelock', async () => { + const { + accessControl, + pausableTester, + owner, + regularAccounts, + pauseManager, + timelockManager, + timelock, + } = await loadFixture(defaultDeploy); + + const pauseAdminRole = await pauseManager.pauseAdminRole(); + const bulkPauseSel = encodeFnSelector('bulkPauseContract(address[])'); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: pauseAdminRole, + targetContract: pauseManager.address, + functionSelector: bulkPauseSel, + grantOperator: owner, + }); + await setPermissionRoleTester( + { accessControl, owner }, + pauseAdminRole, + pauseManager.address, + bulkPauseSel, + [{ account: regularAccounts[0].address, enabled: true }], + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [pauseAdminRole], + [3600], + ); + + await pauseVault({ pauseManager, owner }, pausableTester, { + from: regularAccounts[0], + }); + }); + + it('should fail: when pause delay is no_delay and trying to schedule through timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await scheduleBulkPauseContract(ctx, [fixture.pausableTester.address], { + ...noTimelockDelayRevert(fixture), + }); + }); + + it('when pause delay is changed to custom delay, bulkPauseContract can be scheduled on timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setPauseDelayViaTimelock(ctx, CUSTOM_DELAY); + await scheduleBulkPauseContract(ctx, [fixture.pausableTester.address]); + await increase(CUSTOM_DELAY); + await executeBulkPauseContract(ctx, [fixture.pausableTester.address]); + }); + + it('should fail: when pause delay is custom delay and trying to call directly before delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setPauseDelayViaTimelock(ctx, CUSTOM_DELAY); + + await pauseVault(fixture, fixture.pausableTester, { + ...(await pauseAdminFunctionNotReady(fixture, BULK_PAUSE_CONTRACT_SEL)), + }); + }); + + it('when pause delay is changed to null_delay, bulkPauseContract uses role timelock delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setPauseDelayViaTimelock(ctx, NULL_DELAY); + await setPauseAdminRoleTimelock(fixture); + await scheduleBulkPauseContract(ctx, [fixture.pausableTester.address]); + await increase(ROLE_TIMELOCK_DELAY); + await executeBulkPauseContract(ctx, [fixture.pausableTester.address]); + }); + }); + + describe('bulkUnpauseContract()', () => { + it('should fail: when caller doesnt have pause admin role', async () => { + const { + pausableTester, + regularAccounts, + pauseManager, + ...timelockFixture + } = await loadFixture(defaultDeploy); + + const calldata = pauseManager.interface.encodeFunctionData( + 'bulkUnpauseContract', + [[pausableTester.address]], + ); + + await bulkScheduleTimelockOperationTester( + timelockFixture, + [pauseManager.address], + [calldata], + {}, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('should fail: when caller has only contract pauser role', async () => { + const { + pausableTester, + regularAccounts, + pauseManager, + accessControl, + ...timelockFixture + } = await loadFixture(defaultDeploy); + + await grantContractPauser( + accessControl, + pausableTester, + regularAccounts[0], + ); + + const calldata = pauseManager.interface.encodeFunctionData( + 'bulkUnpauseContract', + [[pausableTester.address]], + ); + + await bulkScheduleTimelockOperationTester( + { ...timelockFixture, accessControl }, + [pauseManager.address], + [calldata], + {}, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('call from pause admin when not paused', async () => { + const fixture = await loadFixture(defaultDeploy); + + await unpauseContractsViaTimelock(fixture, [ + fixture.pausableTester.address, + ]); + }); + + it('when unpause delay is set, unpause can be scheduled on timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + + await pauseVault(fixture, fixture.pausableTester); + await unpauseContractsViaTimelock(fixture, [ + fixture.pausableTester.address, + ]); + }); + + it('should fail: when trying to call directly before delay', async () => { + const { pausableTester, pauseManager, owner, accessControl } = + await loadFixture(defaultDeploy); + + await pauseVault({ pauseManager, owner }, pausableTester); + + await unpauseVault({ pauseManager, owner }, pausableTester, { + revertCustomError: { + contract: accessControl, + customErrorName: 'SenderIsNotTimelock', + args: [ + await pauseManager.pauseAdminRole(), + BULK_UNPAUSE_CONTRACT_SEL, + owner.address, + ], + }, + }); + }); + + it('when unpause delay is changed to no_delay, bulkUnpauseContract can be called directly', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, NO_DELAY); + await pauseVault(fixture, fixture.pausableTester); + await unpauseVault(fixture, fixture.pausableTester); + }); + + it('should fail: when unpause delay is no_delay and trying to schedule through timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, NO_DELAY); + await pauseVault(fixture, fixture.pausableTester); + + await scheduleBulkUnpauseContract(ctx, [fixture.pausableTester.address], { + ...noTimelockDelayRevert(fixture), + }); + }); + + it('when unpause delay is changed to null_delay, bulkUnpauseContract uses role timelock delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, NULL_DELAY); + await setPauseAdminRoleTimelock(fixture); + await pauseVault(fixture, fixture.pausableTester); + await scheduleBulkUnpauseContract(ctx, [fixture.pausableTester.address]); + await increase(ROLE_TIMELOCK_DELAY); + await executeBulkUnpauseContract(ctx, [fixture.pausableTester.address]); + }); + + it('when unpause delay is changed to custom delay, bulkUnpauseContract can be scheduled on timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, CUSTOM_DELAY); + await pauseVault(fixture, fixture.pausableTester); + await scheduleBulkUnpauseContract(ctx, [fixture.pausableTester.address]); + await increase(CUSTOM_DELAY); + await executeBulkUnpauseContract(ctx, [fixture.pausableTester.address]); + }); + + it('should fail: when unpause delay is custom delay and trying to call directly before delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, CUSTOM_DELAY); + await pauseVault(fixture, fixture.pausableTester); + + await unpauseVault(fixture, fixture.pausableTester, { + ...(await pauseAdminFunctionNotReady( + fixture, + BULK_UNPAUSE_CONTRACT_SEL, + )), + }); + }); + }); + + describe('bulkPauseContractFn()', () => { + const depositRequestSel = DEPOSIT_REQUEST_SEL; + + it('should fail: when caller doesnt have pause admin role', async () => { + const { pausableTester, regularAccounts, pauseManager, owner } = + await loadFixture(defaultDeploy); + + await pauseVaultFn( + { pauseManager, owner }, + pausableTester, + depositRequestSel, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('should fail: when caller has only contract pauser role', async () => { + const { + pausableTester, + regularAccounts, + pauseManager, + owner, + accessControl, + } = await loadFixture(defaultDeploy); + + await grantContractPauser( + accessControl, + pausableTester, + regularAccounts[0], + ); + + await pauseVaultFn( + { pauseManager, owner }, + pausableTester, + depositRequestSel, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('call from pause admin when already paused', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( + defaultDeploy, + ); + + await pauseVaultFn( + { pauseManager, owner }, + pausableTester, + depositRequestSel, + ); + await pauseVaultFn( + { pauseManager, owner }, + pausableTester, + depositRequestSel, + ); + }); + + it('call from pause admin', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( + defaultDeploy, + ); + + await pauseVaultFn( + { pauseManager, owner }, + pausableTester, + depositRequestSel, + ); + }); + + it('when pause admin has function scoped timelock', async () => { + const { + accessControl, + pausableTester, + owner, + regularAccounts, + pauseManager, + timelockManager, + timelock, + } = await loadFixture(defaultDeploy); + + const pauseAdminRole = await pauseManager.pauseAdminRole(); + const bulkPauseFnSel = encodeFnSelector( + 'bulkPauseContractFn(address[],bytes4[])', + ); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: pauseAdminRole, + targetContract: pauseManager.address, + functionSelector: bulkPauseFnSel, + grantOperator: owner, + }); + await setPermissionRoleTester( + { accessControl, owner }, + pauseAdminRole, + pauseManager.address, + bulkPauseFnSel, + [{ account: regularAccounts[0].address, enabled: true }], + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [pauseAdminRole], + [3600], + ); + + await pauseVaultFn( + { pauseManager, owner }, + pausableTester, + depositRequestSel, + { from: regularAccounts[0] }, + ); + }); + + it('should fail: when pause delay is no_delay and trying to schedule through timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await scheduleBulkPauseContractFn( + ctx, + [fixture.pausableTester.address], + [depositRequestSel], + noTimelockDelayRevert(fixture), + ); + }); + + it('when pause delay is changed to custom delay, bulkPauseContractFn can be scheduled on timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setPauseDelayViaTimelock(ctx, CUSTOM_DELAY); + await scheduleBulkPauseContractFn( + ctx, + [fixture.pausableTester.address], + [depositRequestSel], + ); + await increase(CUSTOM_DELAY); + await executeBulkPauseContractFn( + ctx, + [fixture.pausableTester.address], + [depositRequestSel], + ); + }); + + it('should fail: when pause delay is custom delay and trying to call directly before delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setPauseDelayViaTimelock(ctx, CUSTOM_DELAY); + + await pauseVaultFn(fixture, fixture.pausableTester, depositRequestSel, { + ...(await pauseAdminFunctionNotReady( + fixture, + BULK_PAUSE_CONTRACT_FN_SEL, + )), + }); + }); + + it('when pause delay is changed to null_delay, bulkPauseContractFn uses role timelock delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setPauseDelayViaTimelock(ctx, NULL_DELAY); + await setPauseAdminRoleTimelock(fixture); + await scheduleBulkPauseContractFn( + ctx, + [fixture.pausableTester.address], + [depositRequestSel], + ); + await increase(ROLE_TIMELOCK_DELAY); + await executeBulkPauseContractFn( + ctx, + [fixture.pausableTester.address], + [depositRequestSel], + ); + }); + }); + + describe('bulkUnpauseContractFn()', () => { + const depositRequestSel = DEPOSIT_REQUEST_SEL; + + it('should fail: when caller doesnt have pause admin role', async () => { + const { + pausableTester, + regularAccounts, + pauseManager, + ...timelockFixture + } = await loadFixture(defaultDeploy); + + const calldata = pauseManager.interface.encodeFunctionData( + 'bulkUnpauseContractFn', + [[pausableTester.address], [depositRequestSel]], + ); + + await bulkScheduleTimelockOperationTester( + timelockFixture, + [pauseManager.address], + [calldata], + {}, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('should fail: when caller has only contract pauser role', async () => { + const { + pausableTester, + regularAccounts, + pauseManager, + accessControl, + ...timelockFixture + } = await loadFixture(defaultDeploy); + + await grantContractPauser( + accessControl, + pausableTester, + regularAccounts[0], + ); + + const calldata = pauseManager.interface.encodeFunctionData( + 'bulkUnpauseContractFn', + [[pausableTester.address], [depositRequestSel]], + ); + + await bulkScheduleTimelockOperationTester( + { ...timelockFixture, accessControl }, + [pauseManager.address], + [calldata], + {}, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('call from pause admin when not paused', async () => { + const fixture = await loadFixture(defaultDeploy); + + await unpauseContractFnsViaTimelock( + fixture, + [fixture.pausableTester.address], + [depositRequestSel], + ); + }); + + it('when unpause delay is set, unpause can be scheduled on timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + + await pauseVaultFn(fixture, fixture.pausableTester, depositRequestSel); + await unpauseContractFnsViaTimelock( + fixture, + [fixture.pausableTester.address], + [depositRequestSel], + ); + }); + + it('should fail: when trying to call directly before delay', async () => { + const { pausableTester, pauseManager, owner, accessControl } = + await loadFixture(defaultDeploy); + + await pauseVaultFn( + { pauseManager, owner }, + pausableTester, + depositRequestSel, + ); + + await unpauseVaultFn( + { pauseManager, owner }, + pausableTester, + depositRequestSel, + { + revertCustomError: { + contract: accessControl, + customErrorName: 'SenderIsNotTimelock', + args: [ + await pauseManager.pauseAdminRole(), + BULK_UNPAUSE_CONTRACT_FN_SEL, + owner.address, + ], + }, + }, + ); + }); + + it('when unpause delay is changed to no_delay, bulkUnpauseContractFn can be called directly', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, NO_DELAY); + await pauseVaultFn(fixture, fixture.pausableTester, depositRequestSel); + await unpauseVaultFn(fixture, fixture.pausableTester, depositRequestSel); + }); + + it('should fail: when unpause delay is no_delay and trying to schedule through timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, NO_DELAY); + await pauseVaultFn(fixture, fixture.pausableTester, depositRequestSel); + + await scheduleBulkUnpauseContractFn( + ctx, + [fixture.pausableTester.address], + [depositRequestSel], + noTimelockDelayRevert(fixture), + ); + }); + + it('when unpause delay is changed to null_delay, bulkUnpauseContractFn uses role timelock delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, NULL_DELAY); + await setPauseAdminRoleTimelock(fixture); + await pauseVaultFn(fixture, fixture.pausableTester, depositRequestSel); + await scheduleBulkUnpauseContractFn( + ctx, + [fixture.pausableTester.address], + [depositRequestSel], + ); + await increase(ROLE_TIMELOCK_DELAY); + await executeBulkUnpauseContractFn( + ctx, + [fixture.pausableTester.address], + [depositRequestSel], + ); + }); + + it('when unpause delay is changed to custom delay, bulkUnpauseContractFn can be scheduled on timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, CUSTOM_DELAY); + await pauseVaultFn(fixture, fixture.pausableTester, depositRequestSel); + await scheduleBulkUnpauseContractFn( + ctx, + [fixture.pausableTester.address], + [depositRequestSel], + ); + await increase(CUSTOM_DELAY); + await executeBulkUnpauseContractFn( + ctx, + [fixture.pausableTester.address], + [depositRequestSel], + ); + }); + + it('should fail: when unpause delay is custom delay and trying to call directly before delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, CUSTOM_DELAY); + await pauseVaultFn(fixture, fixture.pausableTester, depositRequestSel); + + await unpauseVaultFn(fixture, fixture.pausableTester, depositRequestSel, { + ...(await pauseAdminFunctionNotReady( + fixture, + BULK_UNPAUSE_CONTRACT_FN_SEL, + )), + }); + }); + }); + + describe('contractAdminPause()', () => { + it('should fail: when caller doesnt have contract pauser role', async () => { + const { pausableTester, regularAccounts, pauseManager, owner } = + await loadFixture(defaultDeploy); + + await adminPauseContractTest({ pauseManager, owner }, pausableTester, { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }); + }); + + it('should fail: when caller has only pause admin role', async () => { + const { + pausableTester, + regularAccounts, + pauseManager, + owner, + accessControl, + } = await loadFixture(defaultDeploy); + + const pauseAdminRole = await pauseManager.pauseAdminRole(); + await accessControl['grantRole(bytes32,address)']( + pauseAdminRole, + regularAccounts[0].address, + ); + + await adminPauseContractTest({ pauseManager, owner }, pausableTester, { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }); + }); + + it('should fail: when role is user facing', async () => { + const { pausableTester, pauseManager, owner, roles } = await loadFixture( + defaultDeploy, + ); + + await pausableTester.setContractAdminRole(roles.common.greenlisted); + + await adminPauseContractTest({ pauseManager, owner }, pausableTester, { + revertCustomError: { + customErrorName: 'UserFacingRoleNotAllowed', + args: [roles.common.greenlisted], + }, + }); + }); + + it('call from contract pauser when already paused', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( + defaultDeploy, + ); + + await adminPauseContractTest({ pauseManager, owner }, pausableTester); + await adminPauseContractTest({ pauseManager, owner }, pausableTester); + }); + + it('call from contract pauser', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( + defaultDeploy, + ); + + await adminPauseContractTest({ pauseManager, owner }, pausableTester); + }); + + it('when contract pauser has function scoped access with timelock', async () => { + const { + accessControl, + pausableTester, + owner, + regularAccounts, + pauseManager, + timelockManager, + timelock, + roles, + } = await loadFixture(defaultDeploy); + + await pausableTester.setContractAdminRole( + roles.common.blacklistedOperator, + ); + const contractPauserRole = await pausableTester.contractAdminRole(); + const pauseSel = encodeFnSelector('contractAdminPause(address)'); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: contractPauserRole, + targetContract: pauseManager.address, + functionSelector: pauseSel, + grantOperator: owner, + }); + await setPermissionRoleTester( + { accessControl, owner }, + contractPauserRole, + pauseManager.address, + pauseSel, + [{ account: regularAccounts[0].address, enabled: true }], + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [contractPauserRole], + [3600], + ); + + await adminPauseContractTest({ pauseManager, owner }, pausableTester, { + from: regularAccounts[0], + }); + }); + + it('should fail: when pause delay is no_delay and trying to schedule through timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await scheduleContractAdminPause( + ctx, + fixture.pausableTester.address, + noTimelockDelayRevert(fixture), + ); + }); + + it('when pause delay is changed to custom delay, contractAdminPause can be scheduled on timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setPauseDelayViaTimelock(ctx, CUSTOM_DELAY); + await scheduleContractAdminPause(ctx, fixture.pausableTester.address); + await increase(CUSTOM_DELAY); + await executeContractAdminPause(ctx, fixture.pausableTester.address); + }); + + it('should fail: when pause delay is custom delay and trying to call directly before delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setPauseDelayViaTimelock(ctx, CUSTOM_DELAY); + + await adminPauseContractTest(fixture, fixture.pausableTester, { + ...(await contractPauserFunctionNotReady( + fixture, + CONTRACT_ADMIN_PAUSE_SEL, + )), + }); + }); + + it('when pause delay is changed to null_delay, contractAdminPause uses role timelock delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setPauseDelayViaTimelock(ctx, NULL_DELAY); + await setContractPauserRoleTimelock(fixture); + await scheduleContractAdminPause(ctx, fixture.pausableTester.address); + await increase(ROLE_TIMELOCK_DELAY); + await executeContractAdminPause(ctx, fixture.pausableTester.address); + }); + }); + + describe('contractAdminUnpause()', () => { + it('should fail: when caller doesnt have contract pauser role', async () => { + const { + pausableTester, + regularAccounts, + pauseManager, + ...timelockFixture + } = await loadFixture(defaultDeploy); + + const calldata = pauseManager.interface.encodeFunctionData( + 'contractAdminUnpause', + [pausableTester.address], + ); + + await bulkScheduleTimelockOperationTester( + timelockFixture, + [pauseManager.address], + [calldata], + {}, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('should fail: when caller has only pause admin role', async () => { + const { + pausableTester, + regularAccounts, + pauseManager, + accessControl, + ...timelockFixture + } = await loadFixture(defaultDeploy); + + const pauseAdminRole = await pauseManager.pauseAdminRole(); + await accessControl['grantRole(bytes32,address)']( + pauseAdminRole, + regularAccounts[0].address, + ); + + const calldata = pauseManager.interface.encodeFunctionData( + 'contractAdminUnpause', + [pausableTester.address], + ); + + await bulkScheduleTimelockOperationTester( + { ...timelockFixture, accessControl }, + [pauseManager.address], + [calldata], + {}, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('call from contract pauser when not paused', async () => { + const fixture = await loadFixture(defaultDeploy); + + await contractAdminUnpauseViaTimelock(fixture, fixture.pausableTester); + }); + + it('when unpause delay is set, unpause can be scheduled on timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + + await adminPauseContractTest(fixture, fixture.pausableTester); + await contractAdminUnpauseViaTimelock(fixture, fixture.pausableTester); + }); + + it('should fail: when trying to call directly before delay', async () => { + const fixture = await loadFixture(defaultDeploy); + + await adminPauseContractTest(fixture, fixture.pausableTester); + + await adminUnpauseContractTest(fixture, fixture.pausableTester, { + ...(await contractPauserFunctionNotReady( + fixture, + CONTRACT_ADMIN_UNPAUSE_SEL, + )), + }); + }); + + it('when unpause delay is changed to no_delay, contractAdminUnpause can be called directly', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, NO_DELAY); + await adminPauseContractTest(fixture, fixture.pausableTester); + await adminUnpauseContractTest(fixture, fixture.pausableTester); + }); + + it('should fail: when unpause delay is no_delay and trying to schedule through timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, NO_DELAY); + await adminPauseContractTest(fixture, fixture.pausableTester); + + await scheduleContractAdminUnpause( + ctx, + fixture.pausableTester.address, + noTimelockDelayRevert(fixture), + ); + }); + + it('when unpause delay is changed to null_delay, contractAdminUnpause uses role timelock delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, NULL_DELAY); + await setContractPauserRoleTimelock(fixture); + await adminPauseContractTest(fixture, fixture.pausableTester); + await scheduleContractAdminUnpause(ctx, fixture.pausableTester.address); + await increase(ROLE_TIMELOCK_DELAY); + await executeContractAdminUnpause(ctx, fixture.pausableTester.address); + }); + + it('when unpause delay is changed to custom delay, contractAdminUnpause can be scheduled on timelock', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, CUSTOM_DELAY); + await adminPauseContractTest(fixture, fixture.pausableTester); + await scheduleContractAdminUnpause(ctx, fixture.pausableTester.address); + await increase(CUSTOM_DELAY); + await executeContractAdminUnpause(ctx, fixture.pausableTester.address); + }); + + it('should fail: when unpause delay is custom delay and trying to call directly before delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const ctx = toTimelockCtx(fixture); + + await setUnpauseDelayViaTimelock(ctx, CUSTOM_DELAY); + await adminPauseContractTest(fixture, fixture.pausableTester); + + await adminUnpauseContractTest(fixture, fixture.pausableTester, { + ...(await contractPauserFunctionNotReady( + fixture, + CONTRACT_ADMIN_UNPAUSE_SEL, + )), + }); + }); + }); + + describe('setPauseDelay()', () => { + it('should fail: when caller doesnt have pause admin role', async () => { + const { pauseManager, regularAccounts, ...timelockFixture } = + await loadFixture(defaultDeploy); + + const calldata = pauseManager.interface.encodeFunctionData( + 'setPauseDelay', + [NO_DELAY], + ); + + await bulkScheduleTimelockOperationTester( + timelockFixture, + [pauseManager.address], + [calldata], + {}, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('should fail: when trying to call directly before delay', async () => { + const { pauseManager, owner } = await loadFixture(defaultDeploy); + + await expect(pauseManager.connect(owner).setPauseDelay(3600)) + .revertedWithCustomError(pauseManager, 'SenderIsNotTimelock') + .withArgs( + await pauseManager.pauseAdminRole(), + encodeFnSelector('setPauseDelay(uint32)'), + owner.address, + ); + }); + + it('call from pause admin after delay', async () => { + const fixture = await loadFixture(defaultDeploy); + + await setPauseDelayViaTimelock(fixture, 3600); + }); + }); + + describe('setUnpauseDelay()', () => { + it('should fail: when caller doesnt have pause admin role', async () => { + const { pauseManager, regularAccounts, ...timelockFixture } = + await loadFixture(defaultDeploy); + + const calldata = pauseManager.interface.encodeFunctionData( + 'setUnpauseDelay', + [DEFAULT_UNPAUSE_DELAY], + ); + + await bulkScheduleTimelockOperationTester( + timelockFixture, + [pauseManager.address], + [calldata], + {}, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('should fail: when trying to call directly before delay', async () => { + const { pauseManager, owner } = await loadFixture(defaultDeploy); + + await expect( + pauseManager.connect(owner).setUnpauseDelay(DEFAULT_UNPAUSE_DELAY * 2), + ) + .revertedWithCustomError(pauseManager, 'SenderIsNotTimelock') + .withArgs( + await pauseManager.pauseAdminRole(), + encodeFnSelector('setUnpauseDelay(uint32)'), + owner.address, + ); + }); + + it('call from pause admin after delay', async () => { + const fixture = await loadFixture(defaultDeploy); + + await setUnpauseDelayViaTimelock(fixture, DEFAULT_UNPAUSE_DELAY * 2); + }); + + it('when unpause delay is changed, unpause uses new delay', async () => { + const fixture = await loadFixture(defaultDeploy); + const newUnpauseDelay = DEFAULT_UNPAUSE_DELAY * 2; + + await setUnpauseDelayViaTimelock(fixture, newUnpauseDelay); + await pauseGlobalTest(fixture); + + const calldata = + fixture.pauseManager.interface.encodeFunctionData('globalUnpause'); + + await bulkScheduleTimelockOperationTester( + fixture, + [fixture.pauseManager.address], + [calldata], + ); + await increase(DEFAULT_UNPAUSE_DELAY); + + await executeTimelockOperationTester( + fixture, + fixture.pauseManager.address, + calldata, + fixture.owner.address, + { + revertCustomError: { + contract: fixture.timelockManager, + customErrorName: 'TimelockOperationNotReady', + }, + }, + ); + + await increase(DEFAULT_UNPAUSE_DELAY); + await executeTimelockOperationTester( + fixture, + fixture.pauseManager.address, + calldata, + fixture.owner.address, + ); + }); + }); +}); diff --git a/test/unit/MidasTimelockManager.test.ts b/test/unit/MidasTimelockManager.test.ts new file mode 100644 index 00000000..70917727 --- /dev/null +++ b/test/unit/MidasTimelockManager.test.ts @@ -0,0 +1,4108 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { increase } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time'; +import { days } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time/duration'; +import { expect } from 'chai'; +import { constants, ethers } from 'ethers'; + +import { encodeFnSelector } from '../../helpers/utils'; +import { + MidasTimelockManager, + MidasTimelockManager__factory, + MidasTimelockManagerTest__factory, +} from '../../typechain-types'; +import { + NO_DELAY, + setPermissionRoleTester, + setRoleTimelocksTester, + setupGrantOperatorRole, +} from '../common/ac.helpers'; +import { + asyncForEach, + OptionalCommonParams, + validateImplementation, +} from '../common/common.helpers'; +import { defaultDeploy } from '../common/fixtures'; +import { + abortOperationTest, + executeTimelockOperationTester, + pauseTimelockOperationTest, + bulkScheduleTimelockOperationTester, + scheduleTimelockOperationTester, + setMaxPendingOperationsPerProposerTester, + setSecurityCouncilTest, + voteForExecutionTest, + voteForVetoTest, +} from '../common/timelock-manager.helpers'; + +const executeTimelockOperationSelector = encodeFnSelector( + 'executeTimelockOperation(address,bytes)', +); +const withOnlyContractAdminSelector = encodeFnSelector( + 'withOnlyContractAdmin()', +); + +export const timelockManagerRevert = ( + timelockManager: MidasTimelockManager, + customErrorName: string, + args?: unknown[], +): OptionalCommonParams => ({ + revertCustomError: { + contract: timelockManager, + customErrorName, + args, + }, +}); + +describe('MidasTimelockManager', () => { + it('deployment', async () => { + const { accessControl, timelockManager, timelock, councilMembers } = + await loadFixture(defaultDeploy); + + expect(await timelockManager.accessControl()).to.eq(accessControl.address); + expect(await timelockManager.timelock()).to.eq(timelock.address); + expect(await timelockManager.councilQuorum(0)).to.eq(3); + const councilMembersInContract = + await timelockManager.getSecurityCouncilMembers(0); + expect(await timelockManager.SECURITY_COUNCIL_MIN_MEMBERS()).to.eq(5); + expect(councilMembersInContract.length).to.eq(councilMembers.length); + + for (const member of councilMembersInContract) { + expect(councilMembersInContract.includes(member)).to.eq(true); + } + expect(await timelockManager.EXPIRY_PERIOD()).to.eq(days(45)); + expect(await timelockManager.DISPUTE_PERIOD()).to.eq(days(3)); + expect(await timelockManager.MAX_PENDING_OPERATIONS_PER_PROPOSER()).to.eq( + 100, + ); + expect(await timelockManager.SECURITY_COUNCIL_MAX_MEMBERS()).to.eq(15); + expect(await timelockManager.SECURITY_COUNCIL_MIN_MEMBERS()).to.eq(5); + expect(await timelockManager.maxPendingOperationsPerProposer()).to.eq(100); + + await validateImplementation(MidasTimelockManager__factory); + }); + + describe('initializeTimelock()', () => { + it('should initialize timelock address', async () => { + const { accessControl, timelock, councilMembers, owner } = + await loadFixture(defaultDeploy); + + const timelockManager = await new MidasTimelockManagerTest__factory( + owner, + ).deploy(); + + await timelockManager.initialize( + accessControl.address, + 100, + councilMembers.map((v) => v.address), + ); + + await timelockManager.initializeTimelock(timelock.address); + + expect(await timelockManager.timelock()).to.eq(timelock.address); + }); + + it('should fail: when timelock is already set', async () => { + const { timelockManager, timelock } = await loadFixture(defaultDeploy); + + await expect( + timelockManager.initializeTimelock(timelock.address), + ).to.be.revertedWithCustomError(timelockManager, 'TimelockAlreadySet'); + }); + + it('should fail: when timelock address is zero', async () => { + const { accessControl, councilMembers, owner } = await loadFixture( + defaultDeploy, + ); + + const timelockManager = await new MidasTimelockManagerTest__factory( + owner, + ).deploy(); + + await timelockManager.initialize( + accessControl.address, + 100, + councilMembers.map((v) => v.address), + ); + + await expect(timelockManager.initializeTimelock(constants.AddressZero)) + .to.be.revertedWithCustomError(timelockManager, 'InvalidAddress') + .withArgs(constants.AddressZero); + }); + + it('should fail: when caller do not have DEFAULT_ADMIN_ROLE', async () => { + const { + accessControl, + timelock, + councilMembers, + owner, + regularAccounts, + } = await loadFixture(defaultDeploy); + + const timelockManager = await new MidasTimelockManagerTest__factory( + owner, + ).deploy(); + + await timelockManager.initialize( + accessControl.address, + 100, + councilMembers.map((v) => v.address), + ); + + await expect( + timelockManager + .connect(regularAccounts[0]) + .initializeTimelock(timelock.address), + ).to.be.revertedWithCustomError(timelockManager, 'NoFunctionPermission'); + }); + }); + + describe('scheduleTimelockOperation()', () => { + it('should schedule timelock operation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ); + }); + + it('when same target and calldata was executed it can be scheduled again', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + ); + + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + ); + }); + + it('when too many pending operations for a signle proposer but should affect different proposer', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await accessControl['grantRole(bytes32,address)']( + constants.HashZero, + regularAccounts[0].address, + ); + + await setMaxPendingOperationsPerProposerTester( + { timelockManager, owner, accessControl, timelock }, + 1, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ); + + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + accessControl.interface.encodeFunctionData( + 'grantRole(bytes32,address)', + [constants.HashZero, wAccessControlTester.address], + ), + {}, + { + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when same target and calldata is pending and not yet executed', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + ); + + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + {}, + timelockManagerRevert(timelockManager, 'OperationAlreadyPending'), + ); + }); + + it('should fail: when same target and calldata is pending from another proposer', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await accessControl['grantRole(bytes32,address)']( + constants.HashZero, + regularAccounts[0].address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + ); + + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + {}, + { + ...timelockManagerRevert(timelockManager, 'OperationAlreadyPending'), + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when role do not have timelock delay', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + {}, + timelockManagerRevert(timelockManager, 'NoTimelockDelayForRole'), + ); + }); + + it('should fail: when too many pending operations for a signle proposer', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setMaxPendingOperationsPerProposerTester( + { timelockManager, owner, accessControl, timelock }, + 1, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ); + + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + accessControl.address, + accessControl.interface.encodeFunctionData( + 'grantRole(bytes32,address)', + [constants.HashZero, wAccessControlTester.address], + ), + {}, + timelockManagerRevert(timelockManager, 'TooManyPendingOperations'), + ); + }); + + it('should fail: when required role is user facing', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + roles, + } = await loadFixture(defaultDeploy); + + await wAccessControlTester.setContractAdminRole(roles.common.greenlisted); + + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + {}, + timelockManagerRevert(timelockManager, 'UserFacingRoleNotAllowed', [ + roles.common.greenlisted, + ]), + ); + }); + + it('should fail: when target is timelock address', async () => { + const { timelockManager, timelock, owner, accessControl } = + await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + timelock.address, + '0x', + {}, + timelockManagerRevert(timelockManager, 'InvalidAddress', [ + timelock.address, + ]), + ); + }); + + it('should fail: when trying to schedule on function that is not protected with onlyRole', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = + wAccessControlTester.interface.encodeFunctionData('withUnprotected'); + + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + {}, + timelockManagerRevert( + timelockManager, + 'PreflightCallUnexpectedSuccess', + ), + ); + }); + + it('should fail: when preflight reverted with invalid error', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + roles, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withWrongRolePreflight', + [roles.common.defaultAdmin, 0, false, true], + ); + + await scheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + {}, + timelockManagerRevert(timelockManager, 'InvalidPreflightError'), + ); + }); + }); + + describe('bulkScheduleTimelockOperation()', () => { + it('should schedule timelock operation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + }); + + it('when same target and calldata was executed it can be scheduled again', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + }); + + it('when too many pending operations for a signle proposer but should affect different proposer', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await accessControl['grantRole(bytes32,address)']( + constants.HashZero, + regularAccounts[0].address, + ); + + await setMaxPendingOperationsPerProposerTester( + { timelockManager, owner, accessControl, timelock }, + 1, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [ + accessControl.interface.encodeFunctionData( + 'grantRole(bytes32,address)', + [constants.HashZero, wAccessControlTester.address], + ), + ], + {}, + { + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when same target and calldata is pending and not yet executed', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + {}, + timelockManagerRevert(timelockManager, 'OperationAlreadyPending'), + ); + }); + + it('should fail: when same target and calldata is pending from another proposer', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await accessControl['grantRole(bytes32,address)']( + constants.HashZero, + regularAccounts[0].address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + {}, + { + ...timelockManagerRevert(timelockManager, 'OperationAlreadyPending'), + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when role do not have timelock delay', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + {}, + timelockManagerRevert(timelockManager, 'NoTimelockDelayForRole'), + ); + }); + + it('should fail: when too many pending operations for a signle proposer', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setMaxPendingOperationsPerProposerTester( + { timelockManager, owner, accessControl, timelock }, + 1, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + [ + accessControl.interface.encodeFunctionData( + 'grantRole(bytes32,address)', + [constants.HashZero, wAccessControlTester.address], + ), + ], + {}, + timelockManagerRevert(timelockManager, 'TooManyPendingOperations'), + ); + }); + + it('should fail: when required role is user facing', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + roles, + } = await loadFixture(defaultDeploy); + + await wAccessControlTester.setContractAdminRole(roles.common.greenlisted); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + {}, + timelockManagerRevert(timelockManager, 'UserFacingRoleNotAllowed', [ + roles.common.greenlisted, + ]), + ); + }); + + it('should fail: when target is timelock address', async () => { + const { timelockManager, timelock, owner, accessControl } = + await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [timelock.address], + ['0x'], + {}, + timelockManagerRevert(timelockManager, 'InvalidAddress', [ + timelock.address, + ]), + ); + }); + + it('should schedule multiple timelock operations in one transaction', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + const grantRoleCalldata = accessControl.interface.encodeFunctionData( + 'grantRole(bytes32,address)', + [constants.HashZero, wAccessControlTester.address], + ); + + const operationIds = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address, accessControl.address], + [calldata, grantRoleCalldata], + ); + + expect(operationIds.length).to.eq(2); + }); + }); + + describe('executeTimelockOperation()', () => { + it('should fail: when operation do not exist', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + ), + }, + ); + }); + + it('should fail: when operation exist but delay is not passed', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + { + ...timelockManagerRevert( + timelockManager, + 'TimelockOperationNotReady', + ), + }, + ); + }); + + it('should fail: when caller does not have contract admin role or function permission for executeTimelockOperation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + regularAccounts, + } = await loadFixture(defaultDeploy); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + { + ...timelockManagerRevert(timelockManager, 'NoFunctionPermission'), + from: regularAccounts[0], + }, + ); + }); + + it('when operation exist and timelock passed and caller has DEFAULT_ADMIN_ROLE role', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await accessControl['grantRole(bytes32,address)']( + constants.HashZero, + regularAccounts[0].address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + {}, + { + from: regularAccounts[0], + }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + regularAccounts[0].address, + { + from: regularAccounts[0], + }, + ); + }); + + it('when operation exist and timelock passed and caller has function permission for executeTimelockOperation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + regularAccounts, + roles, + } = await loadFixture(defaultDeploy); + + const defaultAdminRole = roles.common.defaultAdmin; + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: defaultAdminRole, + targetContract: timelockManager.address, + functionSelector: executeTimelockOperationSelector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + timelockManager.address, + executeTimelockOperationSelector, + [{ account: regularAccounts[0].address, enabled: true }], + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + { + from: regularAccounts[0], + }, + ); + }); + + it('should fail: with SenderIsNotTimelock when calling target directly after delay passed', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await increase(3600); + + await expect(wAccessControlTester.withOnlyContractAdmin()) + .revertedWithCustomError(accessControl, 'SenderIsNotTimelock') + .withArgs( + constants.HashZero, + withOnlyContractAdminSelector, + owner.address, + ); + }); + + it('when same target and calldata was executed and scheduled again it can be executed again', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + ); + }); + + it('should fail: when operation is paused and timelock delay is passed', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + [2], + ), + }, + ); + }); + + it('when set security council operation is executed via timelock', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + councilMembers, + regularAccounts, + } = await loadFixture(defaultDeploy); + + const securityCouncilManagerRole = + await timelockManager.SECURITY_COUNCIL_MANAGER_ROLE(); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [securityCouncilManagerRole], + [3600], + ); + + const newCouncilMembers = [ + councilMembers[0].address, + councilMembers[1].address, + councilMembers[2].address, + councilMembers[3].address, + regularAccounts[0].address, + ]; + const setCouncilCalldata = timelockManager.interface.encodeFunctionData( + 'setSecurityCouncil', + [newCouncilMembers], + ); + + const councilVersionBefore = + await timelockManager.securityCouncilVersion(); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [timelockManager.address], + [setCouncilCalldata], + { isSetCouncilOperation: true }, + ); + + expect(await timelockManager.pendingSetCouncilOperationId()).to.eq( + operationId, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + timelockManager.address, + setCouncilCalldata, + owner.address, + ); + + expect(await timelockManager.pendingSetCouncilOperationId()).to.eq( + constants.HashZero, + ); + expect(await timelockManager.securityCouncilVersion()).to.eq( + councilVersionBefore.add(1), + ); + }); + }); + + describe('setSecurityCouncil()', () => { + it('should set security council', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setSecurityCouncilTest( + { timelockManager, timelock, owner, accessControl }, + [...councilMembers.map((v) => v.address), accessControl.address], + ); + }); + + it('should fail: when members < MIN_MEMBERS', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setSecurityCouncilTest( + { timelockManager, timelock, owner, accessControl }, + [accessControl.address], + timelockManagerRevert( + timelockManager, + 'InvalidSecurityCouncilMembersLength', + ), + ); + }); + + it('should fail: when members > MAX_MEMBERS', async () => { + const { timelockManager, timelock, owner, accessControl } = + await loadFixture(defaultDeploy); + + await setSecurityCouncilTest( + { timelockManager, timelock, owner, accessControl }, + [...Array(16).fill(accessControl.address)], + timelockManagerRevert( + timelockManager, + 'InvalidSecurityCouncilMembersLength', + ), + ); + }); + + it('should fail: when council member address is zero', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setSecurityCouncilTest( + { timelockManager, timelock, owner, accessControl }, + [ + councilMembers[0].address, + councilMembers[1].address, + councilMembers[2].address, + councilMembers[3].address, + constants.AddressZero, + ], + timelockManagerRevert(timelockManager, 'InvalidAddress', [ + constants.AddressZero, + ]), + ); + }); + + it('should fail: when council member is duplicated', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setSecurityCouncilTest( + { timelockManager, timelock, owner, accessControl }, + [ + councilMembers[0].address, + councilMembers[0].address, + councilMembers[1].address, + councilMembers[2].address, + councilMembers[3].address, + ], + timelockManagerRevert(timelockManager, 'InvalidAddress', [ + councilMembers[0].address, + ]), + ); + }); + + it('should fail: when caller do not have SECURITY_COUNCIL_MANAGER_ROLE', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + councilMembers, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await setSecurityCouncilTest( + { timelockManager, timelock, owner, accessControl }, + councilMembers.map((v) => v.address), + { + ...timelockManagerRevert(timelockManager, 'NoFunctionPermission'), + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when pending set council operation exists', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + councilMembers, + regularAccounts, + } = await loadFixture(defaultDeploy); + + const securityCouncilManagerRole = + await timelockManager.SECURITY_COUNCIL_MANAGER_ROLE(); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [securityCouncilManagerRole], + [3600], + ); + + const scheduledCouncilMembers = councilMembers.map((v) => v.address); + const setCouncilCalldata = timelockManager.interface.encodeFunctionData( + 'setSecurityCouncil', + [scheduledCouncilMembers], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [timelockManager.address], + [setCouncilCalldata], + { isSetCouncilOperation: true }, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [securityCouncilManagerRole], + [NO_DELAY], + ); + + await setSecurityCouncilTest( + { timelockManager, timelock, owner, accessControl }, + [ + councilMembers[0].address, + councilMembers[1].address, + councilMembers[2].address, + councilMembers[3].address, + regularAccounts[0].address, + ], + timelockManagerRevert( + timelockManager, + 'PendingSetCouncilOperationExists', + ), + ); + }); + }); + + describe('setMaxPendingOperationsPerProposer()', () => { + it('should set max pending operations per proposer', async () => { + const { timelockManager, timelock, owner, accessControl } = + await loadFixture(defaultDeploy); + + await setMaxPendingOperationsPerProposerTester( + { timelockManager, owner, accessControl, timelock }, + 100, + ); + }); + + it('should fail: when max pending operations per proposer > MAX_PENDING_OPERATIONS_PER_PROPOSER', async () => { + const { timelockManager, timelock, owner, accessControl } = + await loadFixture(defaultDeploy); + + await setMaxPendingOperationsPerProposerTester( + { timelockManager, owner, accessControl, timelock }, + 101, + timelockManagerRevert( + timelockManager, + 'InvalidMaxPendingOperationsPerProposer', + ), + ); + }); + + it('should fail: when max pending operations per proposer is 0', async () => { + const { timelockManager, timelock, owner, accessControl } = + await loadFixture(defaultDeploy); + + await setMaxPendingOperationsPerProposerTester( + { timelockManager, owner, accessControl, timelock }, + 0, + timelockManagerRevert( + timelockManager, + 'InvalidMaxPendingOperationsPerProposer', + ), + ); + }); + + it('should fail: when caller do not have DEFAULT_ADMIN_ROLE', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await setMaxPendingOperationsPerProposerTester( + { timelockManager, owner, accessControl, timelock }, + 50, + { + ...timelockManagerRevert(timelockManager, 'NoFunctionPermission'), + from: regularAccounts[0], + }, + ); + }); + }); + + describe('pauseTimelockOperation()', () => { + it('should pause timelock operation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { + timelockManager, + timelock, + owner, + accessControl, + }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + }); + + it('should fail: when operation is not scheduled', async () => { + const { timelockManager, timelock, owner, accessControl } = + await loadFixture(defaultDeploy); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + constants.HashZero, + undefined, + { + ...timelockManagerRevert(timelockManager, 'OperationNotPending'), + }, + ); + }); + + it('should fail: when operation is already executed', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + { + ...timelockManagerRevert(timelockManager, 'OperationNotPending'), + }, + ); + }); + + it('should fail: when operation was already paused', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + + undefined, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + ), + }, + ); + }); + + it('should fail: when msg.sender do not have a pauser role', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + { + ...timelockManagerRevert(timelockManager, 'NoFunctionPermission'), + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when msg.sender do not have pauser role but do have default admin', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await accessControl['grantRole(bytes32,address)']( + constants.HashZero, + regularAccounts[0].address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + { + ...timelockManagerRevert(timelockManager, 'NoFunctionPermission'), + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when operation is expired (45 days since scheduled)', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await increase(days(45)); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + ), + }, + ); + }); + + it('should fail: when voted for execution (3/5 council members) and status is ApprovedExecution', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + ), + }, + ); + }); + + it('should fail: when voted for veto (3/5 council members) and status is ReadyToAbort', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + ), + }, + ); + }); + }); + + describe('voteForVeto()', () => { + it('should vote for veto', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { + timelockManager, + timelock, + owner, + accessControl, + }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: councilMembers[0], + }, + ); + }); + + it('should fail: when called by an address that is not in council', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + ...timelockManagerRevert(timelockManager, 'NotInSecurityCouncil'), + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when address is part of current council but not the part of the council that was on the moment of pause', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await setSecurityCouncilTest( + { timelockManager, timelock, owner, accessControl }, + [ + councilMembers[0].address, + councilMembers[1].address, + councilMembers[2].address, + councilMembers[3].address, + regularAccounts[0].address, + ], + ); + + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + ...timelockManagerRevert(timelockManager, 'NotInSecurityCouncil'), + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when status is Pending', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + ), + from: councilMembers[0], + }, + ); + }); + + it('should fail: when proposal do not exist', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + councilMembers, + } = await loadFixture(defaultDeploy); + + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + constants.HashZero, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + ), + from: councilMembers[0], + }, + ); + }); + + it('should fail: when user already voted for veto', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: councilMembers[0], + }, + ); + + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + ...timelockManagerRevert(timelockManager, 'AlreadyVoted'), + from: councilMembers[0], + }, + ); + }); + + it('when 3/5 council is voted for veto and then call abortOperation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await abortOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + ); + }); + + it('when 3/5 council is voted for execution, dispute period is not over, and then council 3/5 votes for veto and then call abortOperation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await abortOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + ); + }); + + it('when 3/5 council is voted for execution, dispute period is over, and then council 3/5 votes for veto and then call abortOperation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await increase(days(3)); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await abortOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + ); + }); + + it('when 1/5 council member voted for veto and status remains Paused', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: councilMembers[0], + }, + ); + }); + + it('when status is ApprovedExecution and council member votes for veto', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: councilMembers[3], + }, + ); + }); + + it('should fail: when status is ReadyToAbort', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + [5], + ), + from: councilMembers[3], + }, + ); + }); + }); + + describe('voteForExecution()', () => { + it('should vote for execution', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { + timelockManager, + timelock, + owner, + accessControl, + }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: councilMembers[0], + }, + ); + }); + + it('should fail: when called by an address that is not in council', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + ...timelockManagerRevert(timelockManager, 'NotInSecurityCouncil'), + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when address is part of current council but not the part of the council that was on the moment of pause', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + regularAccounts, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await setSecurityCouncilTest( + { timelockManager, timelock, owner, accessControl }, + [ + councilMembers[0].address, + councilMembers[1].address, + councilMembers[2].address, + councilMembers[3].address, + regularAccounts[0].address, + ], + ); + + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + ...timelockManagerRevert(timelockManager, 'NotInSecurityCouncil'), + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when status is Pending', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + ), + from: councilMembers[0], + }, + ); + }); + + it('should fail: when proposal do not exist', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + councilMembers, + } = await loadFixture(defaultDeploy); + + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + constants.HashZero, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + ), + from: councilMembers[0], + }, + ); + }); + + it('should fail: when proposal already executed', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + ); + + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + ), + from: councilMembers[0], + }, + ); + }); + + it('should fail: when user already voted for veto', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: councilMembers[0], + }, + ); + + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + ...timelockManagerRevert(timelockManager, 'AlreadyVoted'), + from: councilMembers[0], + }, + ); + }); + + it('should fail: when user already voted for execution', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: councilMembers[0], + }, + ); + + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + ...timelockManagerRevert(timelockManager, 'AlreadyVoted'), + from: councilMembers[0], + }, + ); + }); + + it('should fail: when 3/5 council is voted for execution and then call execute as 3 days dispute didnt pass', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + + { + from: member, + }, + ); + }, + true, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + ), + }, + ); + }); + + it('when 3/5 council is voted for execution, wait 3 days and then call execute', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await increase(3600); + await increase(days(3)); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + ); + }); + + it('should fail: when 3/5 council is voted for execution, dispute period is over, and then council 3/5 votes for veto and then call execute operation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await increase(3600); + await increase(days(3)); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + ), + }, + ); + }); + + it('when 1/5 council member voted for execution and status remains Paused', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: councilMembers[0], + }, + ); + }); + + it('should fail: when operation is expired (45 days since scheduled)', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await increase(days(45)); + + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + ...timelockManagerRevert( + timelockManager, + 'UnexpectedOperationStatus', + [6], + ), + from: councilMembers[0], + }, + ); + }); + }); + + describe('abortOperation()', () => { + it('when operation is expired (45 days since scheduled) and call abortOperation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await increase(days(45)); + + await abortOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + ); + }); + + it('should fail: when status is Paused', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await abortOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + timelockManagerRevert(timelockManager, 'UnexpectedOperationStatus', [ + 2, + ]), + ); + }); + + it('should fail: when status is Pending', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await abortOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + timelockManagerRevert(timelockManager, 'UnexpectedOperationStatus', [ + 1, + ]), + ); + }); + + it('should fail: when status is ApprovedExecution', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await abortOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + timelockManagerRevert(timelockManager, 'UnexpectedOperationStatus', [ + 3, + ]), + ); + }); + }); + + describe('getOriginalProposer()', () => { + it('should return proposer for scheduled operation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + expect( + await timelockManager.getOriginalProposer( + wAccessControlTester.address, + calldata, + ), + ).to.eq(owner.address); + }); + }); + + describe('pendingSetCouncilOperationId()', () => { + it('should fail: when pending set council operation already exists', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + councilMembers, + } = await loadFixture(defaultDeploy); + + const securityCouncilManagerRole = + await timelockManager.SECURITY_COUNCIL_MANAGER_ROLE(); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [securityCouncilManagerRole], + [3600], + ); + + const setCouncilCalldata = timelockManager.interface.encodeFunctionData( + 'setSecurityCouncil', + [councilMembers.map((v) => v.address)], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [timelockManager.address], + [setCouncilCalldata], + { isSetCouncilOperation: true }, + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [timelockManager.address], + [setCouncilCalldata], + { isSetCouncilOperation: true }, + timelockManagerRevert( + timelockManager, + 'PendingSetCouncilOperationExists', + ), + ); + }); + }); + + describe('councilQuorum()', () => { + it('should return quorum for initial security council version', async () => { + const { timelockManager } = await loadFixture(defaultDeploy); + + expect(await timelockManager.councilQuorum(0)).to.eq(3); + }); + + it('should return quorum for updated security council version', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setSecurityCouncilTest( + { timelockManager, timelock, owner, accessControl }, + [...councilMembers.map((v) => v.address), accessControl.address], + ); + + expect(await timelockManager.securityCouncilVersion()).to.eq(1); + expect(await timelockManager.councilQuorum(1)).to.eq(4); + }); + }); + + describe('getSecurityCouncilMembers()', () => { + it('should return members for initial security council version', async () => { + const { timelockManager, councilMembers } = await loadFixture( + defaultDeploy, + ); + + const members = await timelockManager.getSecurityCouncilMembers(0); + + expect(members.length).to.eq(councilMembers.length); + for (const member of councilMembers) { + expect(members.includes(member.address)).to.eq(true); + } + }); + + it('should return members for updated security council version', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + councilMembers, + } = await loadFixture(defaultDeploy); + + const newMembers = [ + ...councilMembers.map((v) => v.address), + accessControl.address, + ]; + + await setSecurityCouncilTest( + { timelockManager, timelock, owner, accessControl }, + newMembers, + ); + + const members = await timelockManager.getSecurityCouncilMembers(1); + + expect(members.length).to.eq(newMembers.length); + for (const member of newMembers) { + expect(members.includes(member)).to.eq(true); + } + }); + }); + + describe('getPendingOperations()', () => { + it('should return empty list when no operations are scheduled', async () => { + const { timelockManager } = await loadFixture(defaultDeploy); + + const pending = await timelockManager.getPendingOperations(); + + expect(pending.length).to.eq(0); + }); + + it('should return scheduled operation ids', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + const pending = await timelockManager.getPendingOperations(); + + expect(pending.length).to.eq(1); + expect(pending[0]).to.eq(operationId); + }); + + it('should not return executed operation ids', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + ); + + const pending = await timelockManager.getPendingOperations(); + + expect(pending.length).to.eq(0); + }); + }); + + describe('getOperationId()', () => { + it('should return operation id for scheduled operation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + expect( + await timelockManager.getOperationId( + wAccessControlTester.address, + calldata, + ), + ).to.eq(operationId); + }); + }); + + describe('getOperationStatus()', () => { + it('should return NotExist for unknown operation', async () => { + const { timelockManager } = await loadFixture(defaultDeploy); + + expect( + await timelockManager.getOperationStatus(constants.HashZero), + ).to.eq(0); + }); + + it('should return Pending for scheduled operation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + expect(await timelockManager.getOperationStatus(operationId)).to.eq(1); + }); + + it('should return Paused for paused operation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + expect(await timelockManager.getOperationStatus(operationId)).to.eq(2); + }); + + it('should return ReadyToExecute when dispute period passed', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await increase(days(3)); + + expect(await timelockManager.getOperationStatus(operationId)).to.eq(4); + }); + + it('should return Expired when expiry period passed', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await increase(days(45)); + + expect(await timelockManager.getOperationStatus(operationId)).to.eq(6); + }); + }); + + describe('getOperationStatusRaw()', () => { + it('should return NotExist for unknown operation', async () => { + const { timelockManager } = await loadFixture(defaultDeploy); + + expect( + await timelockManager.getOperationStatusRaw(constants.HashZero), + ).to.eq(0); + }); + + it('should return stored status without expiry adjustment', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await increase(days(45)); + + expect(await timelockManager.getOperationStatus(operationId)).to.eq(6); + expect(await timelockManager.getOperationStatusRaw(operationId)).to.eq(1); + }); + + it('should return ApprovedExecution after council execution quorum', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await asyncForEach( + councilMembers.slice(0, 3), + async (member) => { + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: member, + }, + ); + }, + true, + ); + + await increase(days(3)); + + expect(await timelockManager.getOperationStatus(operationId)).to.eq(4); + expect(await timelockManager.getOperationStatusRaw(operationId)).to.eq(3); + }); + }); + + describe('getOperationDetails()', () => { + it('should return empty details for unknown operation', async () => { + const { timelockManager } = await loadFixture(defaultDeploy); + + const details = await timelockManager.getOperationDetails( + constants.HashZero, + ); + + expect(details.status).to.eq(0); + expect(details.operationProposer).to.eq(constants.AddressZero); + expect(details.pauser).to.eq(constants.AddressZero); + expect(details.votesForExecution).to.eq(0); + expect(details.votesForVeto).to.eq(0); + }); + + it('should return details for paused operation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + 7, + ); + + const details = await timelockManager.getOperationDetails(operationId); + + expect(details.status).to.eq(2); + expect(details.operationProposer).to.eq(owner.address); + expect(details.pauser).to.eq(owner.address); + expect(details.pauseReasonCode).to.eq(7); + expect(details.votesForExecution).to.eq(0); + expect(details.votesForVeto).to.eq(0); + }); + + it('should return vote counts after council votes', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: councilMembers[0], + }, + ); + + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: councilMembers[1], + }, + ); + + const details = await timelockManager.getOperationDetails(operationId); + + expect(details.status).to.eq(2); + expect(details.votesForExecution).to.eq(1); + expect(details.votesForVeto).to.eq(1); + }); + }); + + describe('getCouncilMemberVoteStatus()', () => { + it('should return false when member did not vote', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + const [votedForExecution, votedForVeto] = + await timelockManager.getCouncilMemberVoteStatus( + operationId, + councilMembers[0].address, + ); + + expect(votedForExecution).to.eq(false); + expect(votedForVeto).to.eq(false); + }); + + it('should return vote flags after member voted', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + councilMembers, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const [operationId] = await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + await pauseTimelockOperationTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + undefined, + ); + + await voteForExecutionTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: councilMembers[0], + }, + ); + + await voteForVetoTest( + { timelockManager, timelock, owner, accessControl }, + operationId, + { + from: councilMembers[1], + }, + ); + + const [member0Execution, member0Veto] = + await timelockManager.getCouncilMemberVoteStatus( + operationId, + councilMembers[0].address, + ); + const [member1Execution, member1Veto] = + await timelockManager.getCouncilMemberVoteStatus( + operationId, + councilMembers[1].address, + ); + + expect(member0Execution).to.eq(true); + expect(member0Veto).to.eq(false); + expect(member1Execution).to.eq(false); + expect(member1Veto).to.eq(true); + }); + }); + + describe('proposerPendingOperationsCount()', () => { + it('should return pending operations count per proposer', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + expect( + await timelockManager.proposerPendingOperationsCount(owner.address), + ).to.eq(0); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [ + wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ), + ], + ); + + expect( + await timelockManager.proposerPendingOperationsCount(owner.address), + ).to.eq(1); + }); + + it('should decrement count when operation is executed', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + expect( + await timelockManager.proposerPendingOperationsCount(owner.address), + ).to.eq(1); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + wAccessControlTester.address, + calldata, + owner.address, + ); + + expect( + await timelockManager.proposerPendingOperationsCount(owner.address), + ).to.eq(0); + }); + }); + + describe('dataHashIndexes()', () => { + it('should return data hash index for scheduled operation', async () => { + const { + timelockManager, + timelock, + owner, + accessControl, + wAccessControlTester, + } = await loadFixture(defaultDeploy); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [constants.HashZero], + [3600], + ); + + const calldata = wAccessControlTester.interface.encodeFunctionData( + 'withOnlyContractAdmin', + ); + const dataWithCaller = ethers.utils.solidityPack( + ['bytes', 'address'], + [calldata, owner.address], + ); + const dataHash = ethers.utils.solidityKeccak256( + ['address', 'uint256', 'bytes'], + [wAccessControlTester.address, 0, dataWithCaller], + ); + + expect(await timelockManager.dataHashIndexes(dataHash)).to.eq(0); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [wAccessControlTester.address], + [calldata], + ); + + expect(await timelockManager.dataHashIndexes(dataHash)).to.eq(0); + }); + }); +}); diff --git a/test/unit/Pausable.test.ts b/test/unit/Pausable.test.ts index 5ea0a089..e000c247 100644 --- a/test/unit/Pausable.test.ts +++ b/test/unit/Pausable.test.ts @@ -2,186 +2,227 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; import { encodeFnSelector } from '../../helpers/utils'; -import { PausableTester__factory } from '../../typechain-types'; +import { acErrors } from '../common/ac.helpers'; import { pauseVault, pauseVaultFn, - unpauseVault, - unpauseVaultFn, + pauseGlobalTest, } from '../common/common.helpers'; import { defaultDeploy } from '../common/fixtures'; describe('Pausable', () => { it('deployment', async () => { - const { pausableTester, roles } = await loadFixture(defaultDeploy); + const { pausableTester, roles, pauseManager } = await loadFixture( + defaultDeploy, + ); - expect(await pausableTester.pauseAdminRole()).eq(roles.common.defaultAdmin); + expect(await pausableTester.contractAdminRole()).eq( + roles.common.defaultAdmin, + ); - expect(await pausableTester.paused()).eq(false); - }); - - it('onlyInitializing', async () => { - const { accessControl, owner } = await loadFixture(defaultDeploy); - - const pausable = await new PausableTester__factory(owner).deploy(); - - await expect( - pausable.initializeWithoutInitializer(accessControl.address), - ).revertedWith('Initializable: contract is not initializing'); + expect( + await pauseManager.isPaused(pausableTester.address, '0x00000000'), + ).eq(false); }); describe('onlyPauseAdmin modifier', async () => { it('should fail: can`t pause if doesn`t have role', async () => { - const { pausableTester, regularAccounts } = await loadFixture( - defaultDeploy, - ); + const { pausableTester, regularAccounts, pauseManager, owner } = + await loadFixture(defaultDeploy); - await pauseVault(pausableTester, { + await pauseVault({ pauseManager, owner }, pausableTester, { from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', + revertCustomError: { + ...acErrors.WMAC_HASNT_PERMISSION(), + contract: pauseManager, + }, }); }); it('can change state if has role', async () => { - const { pausableTester } = await loadFixture(defaultDeploy); + const { pausableTester, pauseManager, owner } = await loadFixture( + defaultDeploy, + ); - await pauseVault(pausableTester); + await pauseVault({ pauseManager, owner }, pausableTester); }); }); - describe('pause()', async () => { - it('fail: can`t pause if caller doesnt have admin role', async () => { - const { pausableTester, regularAccounts } = await loadFixture( - defaultDeploy, + describe('_requireFnNotPaused()', () => { + it('should not fail when fn is not paused', async () => { + const { pausableTester } = await loadFixture(defaultDeploy); + const selector = encodeFnSelector( + 'depositRequest(address,uint256,bytes32)', ); - await pauseVault(pausableTester, { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }); + await expect(pausableTester.requireFnNotPaused(selector)).not.reverted; }); - it('fail: when paused', async () => { + it('should not fail when contract is not paused', async () => { const { pausableTester } = await loadFixture(defaultDeploy); - await pauseVault(pausableTester); - await pauseVault(pausableTester, { - revertMessage: 'Pausable: paused', - }); + await expect( + pausableTester.requireFnNotPaused(encodeFnSelector('randomSelector()')), + ).not.reverted; }); - it('when not paused and caller is admin', async () => { - const { pausableTester } = await loadFixture(defaultDeploy); + it('should not fail when globally is not paused', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( + defaultDeploy, + ); - await pauseVault(pausableTester); + await expect( + pausableTester.requireFnNotPaused(encodeFnSelector('randomSelector()')), + ).not.reverted; }); - }); - describe('pauseFn()', async () => { - it('fail: can`t pause if caller doesnt have admin role', async () => { - const { pausableTester, regularAccounts } = await loadFixture( + it('should fail: when fn is paused', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( defaultDeploy, ); - const selector = encodeFnSelector( 'depositRequest(address,uint256,bytes32)', ); - await pauseVaultFn(pausableTester, selector, { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }); - }); + await pauseVaultFn({ pauseManager, owner }, pausableTester, selector); - it('fail: when paused', async () => { - const { pausableTester } = await loadFixture(defaultDeploy); + await expect(pausableTester.requireFnNotPaused(selector)) + .revertedWithCustomError(pausableTester, 'Paused') + .withArgs(pausableTester.address, selector); + }); + it('should not fail when contract is paused', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( + defaultDeploy, + ); const selector = encodeFnSelector( 'depositRequest(address,uint256,bytes32)', ); - await pauseVaultFn(pausableTester, selector); - await pauseVaultFn(pausableTester, selector, { - revertMessage: 'Pausable: fn paused', - }); - }); + await pauseVault({ pauseManager, owner }, pausableTester); - it('when not paused and caller is admin', async () => { - const { pausableTester } = await loadFixture(defaultDeploy); + await expect(pausableTester.requireFnNotPaused(selector)).not.reverted; + }); + it('should not fail when globally is paused', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( + defaultDeploy, + ); const selector = encodeFnSelector( 'depositRequest(address,uint256,bytes32)', ); - await pauseVaultFn(pausableTester, selector); + await pauseGlobalTest({ pauseManager, owner }); + + await expect(pausableTester.requireFnNotPaused(selector)).not.reverted; }); - }); - describe('unpauseFn()', async () => { - it('fail: can`t pause if caller doesnt have admin role', async () => { - const { pausableTester, regularAccounts } = await loadFixture( + it('should fail: when globally, contract and fn are paused', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( defaultDeploy, ); - const selector = encodeFnSelector( 'depositRequest(address,uint256,bytes32)', ); - await unpauseVaultFn(pausableTester, selector, { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }); + await pauseGlobalTest({ pauseManager, owner }); + await pauseVault({ pauseManager, owner }, pausableTester); + await pauseVaultFn({ pauseManager, owner }, pausableTester, selector); + + await expect(pausableTester.requireFnNotPaused(selector)) + .revertedWithCustomError(pausableTester, 'Paused') + .withArgs(pausableTester.address, selector); }); + }); - it('fail: when unpaused', async () => { + describe('_requireNotPaused()', () => { + it('should not fail when fn is not paused', async () => { const { pausableTester } = await loadFixture(defaultDeploy); - const selector = encodeFnSelector( - 'function depositRequest(address,uint256,bytes32)', + 'depositRequest(address,uint256,bytes32)', ); - await unpauseVaultFn(pausableTester, selector, { - revertMessage: 'Pausable: fn unpaused', - }); + await expect(pausableTester.requireNotPaused(selector)).not.reverted; }); - it('when paused and caller is admin', async () => { + it('should not fail when contract is not paused', async () => { const { pausableTester } = await loadFixture(defaultDeploy); + await expect( + pausableTester.requireNotPaused(encodeFnSelector('randomSelector()')), + ).not.reverted; + }); + + it('should not fail when globally is not paused', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( + defaultDeploy, + ); + + await expect( + pausableTester.requireNotPaused(encodeFnSelector('randomSelector()')), + ).not.reverted; + }); + + it('should fail: when fn is paused', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( + defaultDeploy, + ); const selector = encodeFnSelector( 'depositRequest(address,uint256,bytes32)', ); - await pauseVaultFn(pausableTester, selector); - await unpauseVaultFn(pausableTester, selector); + await pauseVaultFn({ pauseManager, owner }, pausableTester, selector); + + await expect(pausableTester.requireNotPaused(selector)) + .revertedWithCustomError(pausableTester, 'Paused') + .withArgs(pausableTester.address, selector); }); - }); - describe('unpause()', async () => { - it('fail: can`t unpause if caller doesnt have admin role', async () => { - const { pausableTester, regularAccounts } = await loadFixture( + it('should fail: when contract is paused', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( defaultDeploy, ); + const selector = encodeFnSelector( + 'depositRequest(address,uint256,bytes32)', + ); - await unpauseVault(pausableTester, { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }); + await pauseVault({ pauseManager, owner }, pausableTester); + + await expect( + pausableTester.requireNotPaused(selector), + ).revertedWithCustomError(pausableTester, 'Paused'); }); - it('fail: when not paused', async () => { - const { pausableTester } = await loadFixture(defaultDeploy); + it('should fail: when globally is paused', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( + defaultDeploy, + ); + const selector = encodeFnSelector( + 'depositRequest(address,uint256,bytes32)', + ); - await unpauseVault(pausableTester, { - revertMessage: 'Pausable: not paused', - }); + await pauseGlobalTest({ pauseManager, owner }); + + await expect( + pausableTester.requireNotPaused(selector), + ).revertedWithCustomError(pausableTester, 'Paused'); }); - it('when paused and caller is admin', async () => { - const { pausableTester } = await loadFixture(defaultDeploy); + it('should fail: when globally, contract and fn are paused', async () => { + const { pausableTester, pauseManager, owner } = await loadFixture( + defaultDeploy, + ); + const selector = encodeFnSelector( + 'depositRequest(address,uint256,bytes32)', + ); + + await pauseGlobalTest({ pauseManager, owner }); + await pauseVault({ pauseManager, owner }, pausableTester); + await pauseVaultFn({ pauseManager, owner }, pausableTester, selector); - await pauseVault(pausableTester); - await unpauseVault(pausableTester); + await expect(pausableTester.requireNotPaused(selector)) + .revertedWithCustomError(pausableTester, 'Paused') + .withArgs(pausableTester.address, selector); }); }); }); diff --git a/test/unit/RateLimitLibrary.test.ts b/test/unit/RateLimitLibrary.test.ts new file mode 100644 index 00000000..83d59063 --- /dev/null +++ b/test/unit/RateLimitLibrary.test.ts @@ -0,0 +1,614 @@ +import { loadFixture, time } from '@nomicfoundation/hardhat-network-helpers'; +import { + days, + hours, + minutes, +} from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time/duration'; +import { expect } from 'chai'; +import { BigNumberish } from 'ethers'; +import { parseUnits } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; + +import { + RateLimitLibraryTester, + RateLimitLibraryTester__factory, +} from '../../typechain-types'; +import { getCurrentBlockTimestamp } from '../common/common.helpers'; +import { + calculateWindowRateLimitCapacity, + mulDiv, +} from '../common/manageable-vault.helpers'; + +type WindowStatus = Awaited< + ReturnType +>[number]; + +describe('RateLimitLibrary', function () { + const WINDOW_1D = days(1); + const WINDOW_2D = days(2); + const MIN_WINDOW = 60; + + const rateLimitLibraryFixture = async () => { + const [signer] = await ethers.getSigners(); + const tester = await new RateLimitLibraryTester__factory(signer).deploy(); + return { tester, signer }; + }; + + const getStatusByWindow = ( + statuses: WindowStatus[], + window: BigNumberish, + ): WindowStatus | undefined => statuses.find((s) => s.window.eq(window)); + + const findOneWeiMulDivSplitGap = ( + limit: BigNumberish, + window: number, + ): { leftElapsed: number; rightElapsed: number } => { + for (let leftElapsed = 1; leftElapsed < window; leftElapsed++) { + for ( + let rightElapsed = 1; + leftElapsed + rightElapsed < window; + rightElapsed++ + ) { + const singleDecay = mulDiv(limit, leftElapsed + rightElapsed, window); + const splitDecay = mulDiv(limit, leftElapsed, window).add( + mulDiv(limit, rightElapsed, window), + ); + if (singleDecay.sub(splitDecay).eq(1)) { + return { leftElapsed, rightElapsed }; + } + } + } + + throw new Error('Failed to find 1 wei mulDiv split gap'); + }; + + const expectStatusMatchesCapacity = async ( + tester: RateLimitLibraryTester, + window: BigNumberish, + storedAmountInFlight: BigNumberish, + limit: BigNumberish, + windowDuration: BigNumberish, + lastUpdated: BigNumberish, + ) => { + const now = await getCurrentBlockTimestamp(); + const { inFlight, remaining } = calculateWindowRateLimitCapacity({ + amountInFlight: storedAmountInFlight, + lastUpdated, + limit, + window: windowDuration, + now, + }); + + const statuses = await tester.getWindowStatusesPublic(); + const status = getStatusByWindow(statuses, window); + + expect(status).not.eq(undefined); + expect(status!.inFlight).eq(inFlight); + expect(status!.remaining).eq(remaining); + expect(status!.limit).eq(limit); + expect(status!.window).eq(windowDuration); + expect(status!.lastUpdated).eq(lastUpdated); + }; + + describe('setWindowLimit()', () => { + it('should add a new window, emit WindowLimitSet and return previousLimit 0', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const limit = parseUnits('1000'); + + await expect(tester.setWindowLimitPublic(WINDOW_1D, limit)) + .to.emit(tester, 'WindowLimitSet') + .withArgs(WINDOW_1D, limit); + + expect(await tester.windowCountPublic()).eq(1); + expect(await tester.hasWindowPublic(WINDOW_1D)).eq(true); + }); + + it('should initialize storage with full remaining capacity', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const limit = parseUnits('1000'); + + await tester.setWindowLimitPublic(WINDOW_1D, limit); + + const [storedLimit, amountInFlight, lastUpdated, windowDuration] = + await tester.getWindowConfigPublic(WINDOW_1D); + + expect(storedLimit).eq(limit); + expect(amountInFlight).eq(0); + expect(windowDuration).eq(WINDOW_1D); + expect(lastUpdated).eq(await getCurrentBlockTimestamp()); + + const statuses = await tester.getWindowStatusesPublic(); + const status = getStatusByWindow(statuses, WINDOW_1D); + + expect(status!.inFlight).eq(0); + expect(status!.remaining).eq(limit); + }); + + it('should allow limit 0', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + + await tester.setWindowLimitPublic(WINDOW_1D, 0); + + const statuses = await tester.getWindowStatusesPublic(); + expect(getStatusByWindow(statuses, WINDOW_1D)!.remaining).eq(0); + }); + + it('should fail: window shorter than 1 minute', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + + await expect(tester.setWindowLimitPublic(MIN_WINDOW - 1, 1)) + .to.be.revertedWithCustomError(tester, 'WindowTooShort') + .withArgs(MIN_WINDOW - 1); + }); + + it('should allow window exactly 1 minute', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + + await expect(tester.setWindowLimitPublic(MIN_WINDOW, 1)).not.reverted; + expect(await tester.hasWindowPublic(MIN_WINDOW)).eq(true); + }); + + it('should return previous limit when updating an existing window', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const initialLimit = parseUnits('1000'); + const newLimit = parseUnits('500'); + + await tester.setWindowLimitPublic(WINDOW_1D, initialLimit); + + await expect(tester.setWindowLimitPublic(WINDOW_1D, newLimit)) + .to.emit(tester, 'WindowLimitSet') + .withArgs(WINDOW_1D, newLimit); + + const previousLimit = await tester.callStatic.setWindowLimitPublic( + WINDOW_1D, + newLimit, + ); + expect(previousLimit).eq(newLimit); + + const [, , , windowDuration] = await tester.getWindowConfigPublic( + WINDOW_1D, + ); + expect(windowDuration).eq(WINDOW_1D); + + const statuses = await tester.getWindowStatusesPublic(); + expect(getStatusByWindow(statuses, WINDOW_1D)!.limit).eq(newLimit); + }); + + it('should checkpoint stored in-flight using the new limit on update', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const initialLimit = parseUnits('1000'); + const newLimit = parseUnits('500'); + const consumed = parseUnits('800'); + + await tester.setWindowLimitPublic(WINDOW_1D, initialLimit); + await tester.consumeLimitPublic(consumed); + + const [, , lastUpdatedAfterConsume] = await tester.getWindowConfigPublic( + WINDOW_1D, + ); + await time.increase(hours(12)); + + await tester.setWindowLimitPublic(WINDOW_1D, newLimit); + + const [, amountInFlight, lastUpdatedAfterSet] = + await tester.getWindowConfigPublic(WINDOW_1D); + + const { inFlight: expectedStored } = calculateWindowRateLimitCapacity({ + amountInFlight: consumed, + lastUpdated: lastUpdatedAfterConsume, + limit: newLimit, + window: WINDOW_1D, + now: lastUpdatedAfterSet, + }); + + expect(amountInFlight).eq(expectedStored); + expect(lastUpdatedAfterSet).eq(await getCurrentBlockTimestamp()); + + const statuses = await tester.getWindowStatusesPublic(); + const status = getStatusByWindow(statuses, WINDOW_1D); + + expect(status!.limit).eq(newLimit); + expect(status!.inFlight).eq(expectedStored); + expect(status!.remaining).eq( + newLimit.lte(expectedStored) ? 0 : newLimit.sub(expectedStored), + ); + }); + + it('should support multiple independent windows', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + + await tester.setWindowLimitPublic(WINDOW_1D, parseUnits('1000')); + await tester.setWindowLimitPublic(WINDOW_2D, parseUnits('2000')); + + expect(await tester.windowCountPublic()).eq(2); + + const statuses = await tester.getWindowStatusesPublic(); + expect(statuses.length).eq(2); + expect(getStatusByWindow(statuses, WINDOW_1D)!.limit).eq( + parseUnits('1000'), + ); + expect(getStatusByWindow(statuses, WINDOW_2D)!.limit).eq( + parseUnits('2000'), + ); + }); + }); + + describe('removeWindowLimit()', () => { + it('should remove an existing window and emit WindowLimitRemoved', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + + await tester.setWindowLimitPublic(WINDOW_1D, parseUnits('1000')); + + await expect(tester.removeWindowLimitPublic(WINDOW_1D)) + .to.emit(tester, 'WindowLimitRemoved') + .withArgs(WINDOW_1D); + + expect(await tester.windowCountPublic()).eq(0); + expect(await tester.hasWindowPublic(WINDOW_1D)).eq(false); + expect((await tester.getWindowStatusesPublic()).length).eq(0); + }); + + it('should fail: unknown window', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + + await expect(tester.removeWindowLimitPublic(WINDOW_1D)) + .to.be.revertedWithCustomError(tester, 'UnknownWindowLimit') + .withArgs(WINDOW_1D); + }); + + it('should fail: removing the same window twice', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + + await tester.setWindowLimitPublic(WINDOW_1D, parseUnits('1000')); + await tester.removeWindowLimitPublic(WINDOW_1D); + + await expect(tester.removeWindowLimitPublic(WINDOW_1D)) + .to.be.revertedWithCustomError(tester, 'UnknownWindowLimit') + .withArgs(WINDOW_1D); + }); + + it('should not affect other windows', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + + await tester.setWindowLimitPublic(WINDOW_1D, parseUnits('1000')); + await tester.setWindowLimitPublic(WINDOW_2D, parseUnits('2000')); + + await tester.removeWindowLimitPublic(WINDOW_1D); + + expect(await tester.windowCountPublic()).eq(1); + expect(await tester.hasWindowPublic(WINDOW_2D)).eq(true); + + const statuses = await tester.getWindowStatusesPublic(); + expect(statuses.length).eq(1); + expect(statuses[0].window).eq(WINDOW_2D); + }); + }); + + describe('consumeLimit()', () => { + it('should consume amount and update stored in-flight', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const limit = parseUnits('1000'); + const amount = parseUnits('300'); + + await tester.setWindowLimitPublic(WINDOW_1D, limit); + await tester.consumeLimitPublic(amount); + + const [, amountInFlight, lastUpdated] = + await tester.getWindowConfigPublic(WINDOW_1D); + + expect(amountInFlight).eq(amount); + expect(lastUpdated).eq(await getCurrentBlockTimestamp()); + + const statuses = await tester.getWindowStatusesPublic(); + const status = getStatusByWindow(statuses, WINDOW_1D); + + expect(status!.inFlight).eq(amount); + expect(status!.remaining).eq(limit.sub(amount)); + }); + + it('should allow consuming the full limit', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const limit = parseUnits('1000'); + + await tester.setWindowLimitPublic(WINDOW_1D, limit); + await tester.consumeLimitPublic(limit); + + const statuses = await tester.getWindowStatusesPublic(); + expect(getStatusByWindow(statuses, WINDOW_1D)!.remaining).eq(0); + expect(getStatusByWindow(statuses, WINDOW_1D)!.inFlight).eq(limit); + }); + + it('should fail: amount exceeds remaining', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const limit = parseUnits('1000'); + + await tester.setWindowLimitPublic(WINDOW_1D, limit); + await tester.consumeLimitPublic(parseUnits('800')); + + await expect( + tester.consumeLimitPublic(parseUnits('500')), + ).to.be.revertedWithCustomError(tester, 'WindowLimitExceeded'); + }); + + it('should fail: any positive consume when limit is 0', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + + await tester.setWindowLimitPublic(WINDOW_1D, 0); + + await expect(tester.consumeLimitPublic(1)) + .to.be.revertedWithCustomError(tester, 'WindowLimitExceeded') + .withArgs(WINDOW_1D, 0, 1); + }); + + it('should allow zero amount consume as checkpoint only', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const limit = parseUnits('1000'); + + await tester.setWindowLimitPublic(WINDOW_1D, limit); + await tester.consumeLimitPublic(parseUnits('500')); + + const [, amountInFlightBefore, lastUpdatedAfterConsume] = + await tester.getWindowConfigPublic(WINDOW_1D); + + await time.increase(hours(6)); + + await tester.consumeLimitPublic(0); + + const [, amountInFlightAfter, lastUpdatedAfterCheckpoint] = + await tester.getWindowConfigPublic(WINDOW_1D); + + const { inFlight: expectedInFlight } = calculateWindowRateLimitCapacity({ + amountInFlight: amountInFlightBefore, + lastUpdated: lastUpdatedAfterConsume, + limit, + window: WINDOW_1D, + now: lastUpdatedAfterCheckpoint, + }); + + expect(amountInFlightAfter).eq(expectedInFlight); + expect(amountInFlightAfter).lt(amountInFlightBefore); + expect(lastUpdatedAfterCheckpoint).eq(await getCurrentBlockTimestamp()); + }); + + it('should charge every configured window', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const amount = parseUnits('100'); + + await tester.setWindowLimitPublic(WINDOW_1D, parseUnits('1000')); + await tester.setWindowLimitPublic(WINDOW_2D, parseUnits('2000')); + + await tester.consumeLimitPublic(amount); + + const statuses = await tester.getWindowStatusesPublic(); + + expect(getStatusByWindow(statuses, WINDOW_1D)!.inFlight).eq(amount); + expect(getStatusByWindow(statuses, WINDOW_2D)!.inFlight).eq(amount); + }); + + it('should fail: when any window lacks headroom', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + + await tester.setWindowLimitPublic(WINDOW_1D, parseUnits('100')); + await tester.setWindowLimitPublic(WINDOW_2D, parseUnits('1000')); + + await expect( + tester.consumeLimitPublic(parseUnits('200')), + ).to.be.revertedWithCustomError(tester, 'WindowLimitExceeded'); + }); + + it('should allow consume after partial linear decay', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const limit = parseUnits('1000'); + + await tester.setWindowLimitPublic(WINDOW_1D, limit); + await tester.consumeLimitPublic(limit); + + await time.increase(hours(1)); + + const statusesBefore = await tester.getWindowStatusesPublic(); + const remainingBefore = getStatusByWindow( + statusesBefore, + WINDOW_1D, + )!.remaining; + + expect(remainingBefore).gt(0); + expect(remainingBefore).lt(limit); + + await tester.consumeLimitPublic(remainingBefore); + + const statusesAfter = await tester.getWindowStatusesPublic(); + const statusAfter = getStatusByWindow(statusesAfter, WINDOW_1D)!; + + expect(statusAfter.inFlight.add(statusAfter.remaining)).eq(limit); + }); + }); + + describe('getWindowStatuses()', () => { + it('should return an empty array when no windows are configured', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + + expect((await tester.getWindowStatusesPublic()).length).eq(0); + }); + + it('should reflect linear decay after time passes', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const limit = parseUnits('1000'); + + await tester.setWindowLimitPublic(WINDOW_1D, limit); + await tester.consumeLimitPublic(limit); + + const [, , lastUpdatedBefore] = await tester.getWindowConfigPublic( + WINDOW_1D, + ); + + await time.increase(hours(1)); + + await expectStatusMatchesCapacity( + tester, + WINDOW_1D, + limit, + limit, + WINDOW_1D, + lastUpdatedBefore, + ); + }); + + it('should restore full remaining after a full window elapses', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const limit = parseUnits('1000'); + + await tester.setWindowLimitPublic(WINDOW_1D, limit); + await tester.consumeLimitPublic(limit); + + await time.increase(WINDOW_1D); + + const statuses = await tester.getWindowStatusesPublic(); + const status = getStatusByWindow(statuses, WINDOW_1D); + + expect(status!.inFlight).eq(0); + expect(status!.remaining).eq(limit); + }); + + it('should return zero remaining when decayed in-flight equals limit', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const limit = parseUnits('1000'); + + await tester.setWindowLimitPublic(WINDOW_1D, limit); + await tester.consumeLimitPublic(limit); + + const statuses = await tester.getWindowStatusesPublic(); + expect(getStatusByWindow(statuses, WINDOW_1D)!.remaining).eq(0); + }); + + it('should return multiple statuses after multiple windows are added', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + + await tester.setWindowLimitPublic(WINDOW_1D, parseUnits('1000')); + await tester.setWindowLimitPublic(WINDOW_2D, parseUnits('2000')); + + const statuses = await tester.getWindowStatusesPublic(); + + expect(statuses.length).eq(2); + + const windows = statuses.map((s) => s.window.toNumber()).sort(); + expect(windows).deep.eq([WINDOW_1D, WINDOW_2D].sort()); + }); + + it('should match decay math when elapsed is between 0 and window', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const limit = parseUnits('100000'); + const window = days(1); + + await tester.setWindowLimitPublic(window, limit); + await tester.consumeLimitPublic(parseUnits('80000')); + + const [, amountInFlight, lastUpdated] = + await tester.getWindowConfigPublic(window); + + await time.increase(minutes(30)); + + await expectStatusMatchesCapacity( + tester, + window, + amountInFlight, + limit, + window, + lastUpdated, + ); + }); + + it('should treat stored in-flight above limit as zero remaining', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const limit = parseUnits('500'); + + await tester.setWindowLimitPublic(WINDOW_1D, parseUnits('1000')); + await tester.consumeLimitPublic(parseUnits('800')); + + await tester.setWindowLimitPublic(WINDOW_1D, limit); + + const statuses = await tester.getWindowStatusesPublic(); + const status = getStatusByWindow(statuses, WINDOW_1D); + + expect(status!.limit).eq(limit); + expect(status!.remaining).eq(0); + }); + + it('should show a 1 wei floor gap between split and single mulDiv', async () => { + const limit = parseUnits('1'); + const window = MIN_WINDOW + 1; + const { leftElapsed, rightElapsed } = findOneWeiMulDivSplitGap( + limit, + window, + ); + + const singleDecay = mulDiv(limit, leftElapsed + rightElapsed, window); + const splitDecay = mulDiv(limit, leftElapsed, window).add( + mulDiv(limit, rightElapsed, window), + ); + + expect(singleDecay.sub(splitDecay)).eq(1); + }); + + it('should reproduce snapshot-based 1 wei drift while on-chain keeps exact stored-state math', async () => { + const { tester } = await loadFixture(rateLimitLibraryFixture); + const limit = parseUnits('1'); + const window = MIN_WINDOW + 1; + const consumed = parseUnits('1'); + const addedAmount = 0; + const { leftElapsed, rightElapsed } = findOneWeiMulDivSplitGap( + limit, + window, + ); + + await tester.setWindowLimitPublic(window, limit); + await tester.consumeLimitPublic(consumed); + + const [, storedBefore, lastUpdatedStoredBefore] = + await tester.getWindowConfigPublic(window); + + await time.increase(leftElapsed); + const t1 = await getCurrentBlockTimestamp(); + + const snapshotAtT1 = getStatusByWindow( + await tester.getWindowStatusesPublic(), + window, + )!; + + await time.increase(rightElapsed); + + await tester.consumeLimitPublic(addedAmount); + + const [, storedAfter, lastUpdatedAfter] = + await tester.getWindowConfigPublic(window); + + const correctAtCheckpoint = calculateWindowRateLimitCapacity({ + amountInFlight: storedBefore, + lastUpdated: lastUpdatedStoredBefore, + limit, + window, + now: lastUpdatedAfter, + }).inFlight; + const expectedStoredAfter = correctAtCheckpoint.add(addedAmount); + + const snapshotBasedAtCheckpoint = calculateWindowRateLimitCapacity({ + amountInFlight: snapshotAtT1.inFlight, + lastUpdated: t1, + limit, + window, + now: lastUpdatedAfter, + }).inFlight; + const naiveExpectedStoredAfter = + snapshotBasedAtCheckpoint.add(addedAmount); + + const driftAbs = expectedStoredAfter.gte(naiveExpectedStoredAfter) + ? expectedStoredAfter.sub(naiveExpectedStoredAfter) + : naiveExpectedStoredAfter.sub(expectedStoredAfter); + expect(driftAbs).eq(1); + expect(storedAfter).eq(expectedStoredAfter); + const onchainVsNaiveAbs = storedAfter.gte(naiveExpectedStoredAfter) + ? storedAfter.sub(naiveExpectedStoredAfter) + : naiveExpectedStoredAfter.sub(storedAfter); + expect(onchainVsNaiveAbs).eq(1); + }); + }); +}); diff --git a/test/unit/RedemptionVault.test.ts b/test/unit/RedemptionVault.test.ts index 424610e4..1e21b612 100644 --- a/test/unit/RedemptionVault.test.ts +++ b/test/unit/RedemptionVault.test.ts @@ -1,7178 +1,237 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; -import { expect } from 'chai'; -import { constants } from 'ethers'; -import { parseUnits } from 'ethers/lib/utils'; -import { ethers } from 'hardhat'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { encodeFnSelector } from '../../helpers/utils'; import { - ManageableVaultTester__factory, - MBasisRedemptionVault__factory, + baseInitParamsRv, + redemptionVaultSuits, +} from './suits/redemption-vault.suits'; + +import { + RedemptionVault__factory, RedemptionVaultTest__factory, } from '../../typechain-types'; -import { acErrors, blackList, greenList } from '../common/ac.helpers'; import { approveBase18, mintToken, - pauseVault, - pauseVaultFn, + validateImplementation, } from '../common/common.helpers'; -import { - setMinGrowthApr, - setRoundDataGrowth, -} from '../common/custom-feed-growth.helpers'; import { setRoundData } from '../common/data-feed.helpers'; import { defaultDeploy, mTokenPermissionedFixture } from '../common/fixtures'; import { addPaymentTokenTest, - addWaivedFeeAccountTest, - changeTokenAllowanceTest, - removePaymentTokenTest, - removeWaivedFeeAccountTest, setInstantFeeTest, - setInstantDailyLimitTest, - setMinAmountTest, - setVariabilityToleranceTest, - withdrawTest, - changeTokenFeeTest, - setTokensReceiverTest, - setFeeReceiverTest, } from '../common/manageable-vault.helpers'; -import { - approveRedeemRequestTest, - redeemFiatRequestTest, - redeemInstantTest, - redeemRequestTest, - rejectRedeemRequestTest, - safeApproveRedeemRequestTest, - safeBulkApproveRequestTest, - setFiatAdditionalFeeTest, - setFiatFlatFeeTest, - setMinFiatRedeemAmountTest, - setRequestRedeemerTest, -} from '../common/redemption-vault.helpers'; -import { sanctionUser } from '../common/with-sanctions-list.helpers'; - -describe('RedemptionVault', function () { - it('deployment', async () => { - const { - redemptionVault, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - roles, - } = await loadFixture(defaultDeploy); - - expect(await redemptionVault.mToken()).eq(mTBILL.address); - - expect(await redemptionVault.ONE_HUNDRED_PERCENT()).eq('10000'); - - expect(await redemptionVault.paused()).eq(false); - - expect(await redemptionVault.tokensReceiver()).eq(tokensReceiver.address); - expect(await redemptionVault.feeReceiver()).eq(feeReceiver.address); - - expect(await redemptionVault.minAmount()).eq(1000); - expect(await redemptionVault.minFiatRedeemAmount()).eq(1000); - - expect(await redemptionVault.instantFee()).eq('100'); - - expect(await redemptionVault.instantDailyLimit()).eq(parseUnits('100000')); - - expect(await redemptionVault.mTokenDataFeed()).eq( - mTokenToUsdDataFeed.address, - ); - expect(await redemptionVault.variationTolerance()).eq(1); - - expect(await redemptionVault.vaultRole()).eq( - roles.tokenRoles.mTBILL.redemptionVaultAdmin, - ); - - expect(await redemptionVault.MANUAL_FULLFILMENT_TOKEN()).eq( - ethers.constants.AddressZero, - ); - }); - - it('failing deployment', async () => { - const { - redemptionVault, - mTokenToUsdDataFeed, - feeReceiver, - tokensReceiver, - mockedSanctionsList, - requestRedeemer, - accessControl, - owner, - mTBILL, - } = await loadFixture(defaultDeploy); - - const redemptionVaultUninitialized = await new RedemptionVaultTest__factory( - owner, - ).deploy(); - - await expect( - redemptionVaultUninitialized.initialize( - ethers.constants.AddressZero, - { - mToken: ethers.constants.AddressZero, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: parseUnits('100'), - }, - requestRedeemer.address, - ), - ).to.be.reverted; - await expect( - redemptionVaultUninitialized.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: ethers.constants.AddressZero, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: parseUnits('100'), - }, - requestRedeemer.address, - ), - ).to.be.reverted; - await expect( - redemptionVaultUninitialized.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: ethers.constants.AddressZero, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: parseUnits('100'), - }, - requestRedeemer.address, - ), - ).to.be.reverted; - await expect( - redemptionVaultUninitialized.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: ethers.constants.AddressZero, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: parseUnits('100'), - }, - requestRedeemer.address, - ), - ).to.be.reverted; - await expect( - redemptionVaultUninitialized.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 10001, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: parseUnits('100'), - }, - requestRedeemer.address, - ), - ).to.be.reverted; - await expect( - redemptionVaultUninitialized.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - { - fiatAdditionalFee: 10001, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: parseUnits('100'), - }, - requestRedeemer.address, - ), - ).to.be.reverted; - - await expect( - redemptionVaultUninitialized.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: parseUnits('100'), - }, - constants.AddressZero, - ), - ).to.be.reverted; - - await expect( - redemptionVault.initializeWithoutInitializer( - ethers.constants.AddressZero, - { - mToken: ethers.constants.AddressZero, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: parseUnits('100'), - }, - requestRedeemer.address, - ), - ).to.be.revertedWith('Initializable: contract is not initializing'); - }); - - describe('MBasisRedemptionVault', () => { - describe('deployment', () => { - it('vaultRole', async () => { - const fixture = await loadFixture(defaultDeploy); - - const tester = await new MBasisRedemptionVault__factory( - fixture.owner, - ).deploy(); - - expect(await tester.vaultRole()).eq( - await tester.M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE(), - ); - }); - }); - }); - - describe('initialization', () => { - it('should fail: cal; initialize() when already initialized', async () => { - const { redemptionVault } = await loadFixture(defaultDeploy); - - await expect( - redemptionVault.initialize( - constants.AddressZero, - { - mToken: constants.AddressZero, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: constants.AddressZero, - tokensReceiver: constants.AddressZero, - }, - { - instantFee: 0, - instantDailyLimit: 0, - }, - constants.AddressZero, - 0, - 0, - { - fiatAdditionalFee: 0, - fiatFlatFee: 0, - minFiatRedeemAmount: 0, - }, - constants.AddressZero, - ), - ).revertedWith('Initializable: contract is already initialized'); - }); - - it('should fail: call with initializing == false', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initializeWithoutInitializer( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('Initializable: contract is not initializing'); - }); - - it('should fail: when _tokensReceiver == address(this)', async () => { - const { - owner, - accessControl, - mTBILL, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: vault.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('invalid address'); - }); - it('should fail: when _feeReceiver == address(this)', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: vault.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('invalid address'); - }); - it('should fail: when limit = 0', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: 0, - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('zero limit'); - }); - it('should fail: when mToken dataFeed address zero', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('zero address'); - }); - it('should fail: when variationTolarance zero', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mockedSanctionsList, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 0, - 1000, - ), - ).revertedWith('fee == 0'); - }); - }); - - describe('setTokensReceiver()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - - await setTokensReceiverTest( - { vault: redemptionVault, owner }, - regularAccounts[0].address, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('should fail: call with zero address receiver', async () => { - const { owner, redemptionVault } = await loadFixture(defaultDeploy); - - await setTokensReceiverTest( - { vault: redemptionVault, owner }, - constants.AddressZero, - { - revertMessage: 'zero address', - }, - ); - }); - - it('should fail: call with address(this) receiver', async () => { - const { owner, redemptionVault } = await loadFixture(defaultDeploy); - - await setTokensReceiverTest( - { vault: redemptionVault, owner }, - redemptionVault.address, - { - revertMessage: 'invalid address', - }, - ); - }); - - it('call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - - await setTokensReceiverTest( - { vault: redemptionVault, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('setMinAmount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - - await setMinAmountTest({ vault: redemptionVault, owner }, 1.1, { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVault } = await loadFixture(defaultDeploy); - await setMinAmountTest({ vault: redemptionVault, owner }, 1.1); - }); - }); - - describe('setMinFiatRedeemAmount()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - - await setMinFiatRedeemAmountTest({ redemptionVault, owner }, 1.1, { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVault } = await loadFixture(defaultDeploy); - await setMinFiatRedeemAmountTest({ redemptionVault, owner }, 1.1); - }); - }); - - describe('setFeeReceiver()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - - await setFeeReceiverTest( - { vault: redemptionVault, owner }, - regularAccounts[0].address, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await setFeeReceiverTest( - { vault: redemptionVault, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('sanctionsListAdminRole()', () => { - it('should return same role as vaultRole()', async () => { - const { redemptionVault } = await loadFixture(defaultDeploy); - const vaultRole = await redemptionVault.vaultRole(); - const sanctionsListAdminRole = - await redemptionVault.sanctionsListAdminRole(); - - expect(sanctionsListAdminRole).eq(vaultRole); - }); - }); - - describe('setFiatFlatFee()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - - await setFiatFlatFeeTest({ redemptionVault, owner }, 100, { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVault } = await loadFixture(defaultDeploy); - await setFiatFlatFeeTest({ redemptionVault, owner }, 100); - }); - }); - - describe('setFiatAdditionalFee()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - - await setFiatAdditionalFeeTest({ redemptionVault, owner }, 100, { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVault } = await loadFixture(defaultDeploy); - await setFiatAdditionalFeeTest({ redemptionVault, owner }, 100); - }); - }); - - describe('setInstantDailyLimit()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - - await setInstantDailyLimitTest( - { vault: redemptionVault, owner }, - parseUnits('1000'), - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('should fail: try to set 0 limit', async () => { - const { owner, redemptionVault } = await loadFixture(defaultDeploy); - - await setInstantDailyLimitTest( - { vault: redemptionVault, owner }, - constants.Zero, - { - revertMessage: 'MV: limit zero', - }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVault } = await loadFixture(defaultDeploy); - await setInstantDailyLimitTest( - { vault: redemptionVault, owner }, - parseUnits('1000'), - ); - }); - }); - - describe('addPaymentToken()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - ethers.constants.AddressZero, - ethers.constants.AddressZero, - 0, - true, - constants.MaxUint256, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when token is already added', async () => { - const { redemptionVault, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - constants.MaxUint256, - { - revertMessage: 'MV: already added', - }, - ); - }); - - it('should fail: when token dataFeed address zero', async () => { - const { redemptionVault, stableCoins, owner } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - constants.AddressZero, - 0, - true, - constants.MaxUint256, - { - revertMessage: 'zero address', - }, - ); - }); - - it('call when allowance is zero', async () => { - const { redemptionVault, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - constants.Zero, - ); - }); - - it('call when allowance is not uint256 max', async () => { - const { redemptionVault, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - parseUnits('100'), - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role and add 3 options on a row', async () => { - const { redemptionVault, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - }); - }); - - describe('addWaivedFeeAccount()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await addWaivedFeeAccountTest( - { vault: redemptionVault, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if account fee already waived', async () => { - const { redemptionVault, owner } = await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVault, owner }, - owner.address, - ); - await addWaivedFeeAccountTest( - { vault: redemptionVault, owner }, - owner.address, - { revertMessage: 'MV: already added' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, owner } = await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVault, owner }, - owner.address, - ); - }); - }); - - describe('removeWaivedFeeAccount()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await removeWaivedFeeAccountTest( - { vault: redemptionVault, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if account not found in restriction', async () => { - const { redemptionVault, owner } = await loadFixture(defaultDeploy); - await removeWaivedFeeAccountTest( - { vault: redemptionVault, owner }, - owner.address, - { revertMessage: 'MV: not found' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, owner } = await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVault, owner }, - owner.address, - ); - await removeWaivedFeeAccountTest( - { vault: redemptionVault, owner }, - owner.address, - ); - }); - }); - - describe('setFee()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await setInstantFeeTest( - { vault: redemptionVault, owner }, - ethers.constants.Zero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: if new value greater then 100%', async () => { - const { redemptionVault, owner } = await loadFixture(defaultDeploy); - await setInstantFeeTest({ vault: redemptionVault, owner }, 10001, { - revertMessage: 'fee > 100%', +import { redeemInstantTest } from '../common/redemption-vault.helpers'; +import { initializeRv } from '../common/vault-initializer.helpers'; + +redemptionVaultSuits( + 'RedemptionVault', + defaultDeploy, + { + createNew: async (owner: SignerWithAddress) => + new RedemptionVaultTest__factory(owner).deploy(), + key: 'redemptionVault', + }, + async () => { + await validateImplementation(RedemptionVault__factory); + }, + { + deployUninitialized: (fixture) => + new RedemptionVaultTest__factory(fixture.owner).deploy(), + initialize: async (fixture, params, opt) => { + await initializeRv( + { ...baseInitParamsRv(fixture), ...params }, + opt?.contract, + opt, + ); + }, + }, + () => { + describe('RedemptionVault', () => { + describe('redeemInstant() with permissioned mToken', () => { + it('with permissioned mToken - burns/transfers mToken from greenlisted user', async () => { + const { + owner, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + mTokenPermissioned, + mTokenPermissionedRoles, + accessControl, + mTokenPermissionedRedemptionVault, + } = await loadFixture(mTokenPermissionedFixture); + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + owner.address, + ); + + await mintToken(mTokenPermissioned, owner, 100_000); + await setInstantFeeTest( + { vault: mTokenPermissionedRedemptionVault, owner }, + 1000, + ); + await approveBase18( + owner, + mTokenPermissioned, + mTokenPermissionedRedemptionVault, + 100_000, + ); + + await mintToken( + stableCoins.dai, + mTokenPermissionedRedemptionVault, + 100_000, + ); + await addPaymentTokenTest( + { vault: mTokenPermissionedRedemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + await redeemInstantTest( + { + redemptionVault: mTokenPermissionedRedemptionVault, + owner, + mTBILL: mTokenPermissioned, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 999, + ); + }); + + it('with permissioned mToken - instant fee is 0, burns/transfers mToken from non-greenlisted user', async () => { + const { + owner, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + mTokenPermissioned, + mTokenPermissionedRoles, + accessControl, + mTokenPermissionedRedemptionVault, + } = await loadFixture(mTokenPermissionedFixture); + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + owner.address, + ); + await mintToken(mTokenPermissioned, owner, 100_000); + await accessControl.revokeRole( + mTokenPermissionedRoles.greenlisted, + owner.address, + ); + await setInstantFeeTest( + { vault: mTokenPermissionedRedemptionVault, owner }, + 0, + ); + await approveBase18( + owner, + mTokenPermissioned, + mTokenPermissionedRedemptionVault, + 100_000, + ); + + await mintToken( + stableCoins.dai, + mTokenPermissionedRedemptionVault, + 100_000, + ); + await addPaymentTokenTest( + { vault: mTokenPermissionedRedemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + await redeemInstantTest( + { + redemptionVault: mTokenPermissionedRedemptionVault, + owner, + mTBILL: mTokenPermissioned, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 999, + ); + }); + + it('with permissioned mToken - redeem instant burns mToken from non-greenlisted user when fee is not 0', async () => { + const { + owner, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + mTokenPermissioned, + mTokenPermissionedRedemptionVault, + mTokenPermissionedRoles, + accessControl, + } = await loadFixture(mTokenPermissionedFixture); + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + owner.address, + ); + await mintToken(mTokenPermissioned, owner, 100_000); + await setInstantFeeTest( + { vault: mTokenPermissionedRedemptionVault, owner }, + 1000, + ); + await accessControl.revokeRole( + mTokenPermissionedRoles.greenlisted, + owner.address, + ); + await approveBase18( + owner, + mTokenPermissioned, + mTokenPermissionedRedemptionVault, + 100_000, + ); + + await mintToken( + stableCoins.dai, + mTokenPermissionedRedemptionVault, + 100_000, + ); + await addPaymentTokenTest( + { vault: mTokenPermissionedRedemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + await redeemInstantTest( + { + redemptionVault: mTokenPermissionedRedemptionVault, + owner, + mTBILL: mTokenPermissioned, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 999, + ); + }); }); }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, owner } = await loadFixture(defaultDeploy); - await setInstantFeeTest({ vault: redemptionVault, owner }, 100); - }); - }); - - describe('setVariabilityTolerance()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await setVariabilityToleranceTest( - { vault: redemptionVault, owner }, - ethers.constants.Zero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if new value zero', async () => { - const { redemptionVault, owner } = await loadFixture(defaultDeploy); - await setVariabilityToleranceTest( - { vault: redemptionVault, owner }, - ethers.constants.Zero, - { revertMessage: 'fee == 0' }, - ); - }); - - it('should fail: if new value greater then 100%', async () => { - const { redemptionVault, owner } = await loadFixture(defaultDeploy); - await setVariabilityToleranceTest( - { vault: redemptionVault, owner }, - 10001, - { revertMessage: 'fee > 100%' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, owner } = await loadFixture(defaultDeploy); - await setVariabilityToleranceTest({ vault: redemptionVault, owner }, 100); - }); - }); - - describe('setRequestRedeemer()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await setRequestRedeemerTest( - { redemptionVault, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if redeemer address zero', async () => { - const { redemptionVault, owner } = await loadFixture(defaultDeploy); - await setRequestRedeemerTest( - { redemptionVault, owner }, - ethers.constants.AddressZero, - { revertMessage: 'zero address' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, owner } = await loadFixture(defaultDeploy); - await setRequestRedeemerTest({ redemptionVault, owner }, owner.address); - }); - }); - - describe('removePaymentToken()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await removePaymentTokenTest( - { vault: redemptionVault, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when token is not exists', async () => { - const { owner, redemptionVault, stableCoins } = await loadFixture( - defaultDeploy, - ); - await removePaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai.address, - { revertMessage: 'MV: not exists' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await removePaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai.address, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role and add 3 options on a row', async () => { - const { redemptionVault, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - - await removePaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai.address, - ); - await removePaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc.address, - ); - await removePaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdt.address, - ); - - await removePaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdt.address, - { revertMessage: 'MV: not exists' }, - ); - }); - }); - - describe('withdrawToken()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await withdrawTest( - { vault: redemptionVault, owner }, - ethers.constants.AddressZero, - 0, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when there is no token in vault', async () => { - const { owner, redemptionVault, regularAccounts, stableCoins } = - await loadFixture(defaultDeploy); - await withdrawTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - 1, - regularAccounts[0], - { revertMessage: 'ERC20: transfer amount exceeds balance' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, regularAccounts, stableCoins, owner } = - await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, redemptionVault, 1); - await withdrawTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - 1, - regularAccounts[0], - ); - }); - }); - - describe('freeFromMinAmount()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVault - .connect(regularAccounts[0]) - .freeFromMinAmount(regularAccounts[1].address, true), - ).to.be.revertedWith('WMAC: hasnt role'); - }); - it('should not fail', async () => { - const { redemptionVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVault.freeFromMinAmount(regularAccounts[0].address, true), - ).to.not.reverted; - - expect( - await redemptionVault.isFreeFromMinAmount(regularAccounts[0].address), - ).to.eq(true); - }); - it('should fail: already in list', async () => { - const { redemptionVault, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVault.freeFromMinAmount(regularAccounts[0].address, true), - ).to.not.reverted; - - expect( - await redemptionVault.isFreeFromMinAmount(regularAccounts[0].address), - ).to.eq(true); - - await expect( - redemptionVault.freeFromMinAmount(regularAccounts[0].address, true), - ).to.revertedWith('DV: already free'); - }); - }); - - describe('changeTokenAllowance()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await changeTokenAllowanceTest( - { vault: redemptionVault, owner }, - ethers.constants.AddressZero, - 0, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: token not exist', async () => { - const { redemptionVault, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - await changeTokenAllowanceTest( - { vault: redemptionVault, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: token not exists' }, - ); - }); - it('should fail: allowance zero', async () => { - const { redemptionVault, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVault, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: zero allowance' }, - ); - }); - it('should fail: if mint exceed allowance', async () => { - const { - redemptionVault, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, owner, 100000); - await approveBase18(owner, stableCoins.dai, redemptionVault, 100000); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVault, owner }, - stableCoins.dai.address, - 100, - ); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVault, owner }, - stableCoins.dai.address, - 100000000, - ); - }); - it('should decrease if allowance < UINT_MAX', async () => { - const { - redemptionVault, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100000); - await approveBase18(owner, mTBILL, redemptionVault, 100000); - - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVault, owner }, - stableCoins.dai.address, - parseUnits('1000'), - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - const tokenConfigBefore = await redemptionVault.tokensConfig( - stableCoins.dai.address, - ); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 999, - ); - - const tokenConfigAfter = await redemptionVault.tokensConfig( - stableCoins.dai.address, - ); - - expect(tokenConfigBefore.allowance.sub(tokenConfigAfter.allowance)).eq( - parseUnits('999'), - ); - }); - it('should not decrease if allowance = UINT_MAX', async () => { - const { - redemptionVault, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100000); - await approveBase18(owner, mTBILL, redemptionVault, 100000); - - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVault, owner }, - stableCoins.dai.address, - constants.MaxUint256, - ); - - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - const tokenConfigBefore = await redemptionVault.tokensConfig( - stableCoins.dai.address, - ); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 999, - ); - - const tokenConfigAfter = await redemptionVault.tokensConfig( - stableCoins.dai.address, - ); - - expect(tokenConfigBefore.allowance).eq(tokenConfigAfter.allowance); - }); - }); - - describe('changeTokenFee()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, regularAccounts, owner } = await loadFixture( - defaultDeploy, - ); - await changeTokenFeeTest( - { vault: redemptionVault, owner }, - ethers.constants.AddressZero, - 0, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: token not exist', async () => { - const { redemptionVault, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - await changeTokenFeeTest( - { vault: redemptionVault, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: token not exists' }, - ); - }); - it('should fail: fee > 100%', async () => { - const { redemptionVault, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: redemptionVault, owner }, - stableCoins.dai.address, - 10001, - { revertMessage: 'fee > 100%' }, - ); - }); - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVault, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: redemptionVault, owner }, - stableCoins.dai.address, - 100, - ); - }); - }); - - describe('redeemInstant()', () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 0, - { - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVault, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemInstant(address,uint256,uint256)', - ); - await pauseVaultFn(redemptionVault, selector); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: burn amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mockedAggregatorMToken, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, redemptionVault, 10); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(mTBILL, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 0); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minAmount', async () => { - const { - redemptionVault, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 99_999, - { - revertMessage: 'RV: amount < min', - }, - ); - }); - - it('should fail: if exceed allowance of deposit by token', async () => { - const { - redemptionVault, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(mTBILL, owner, 100_000); - await changeTokenAllowanceTest( - { vault: redemptionVault, owner }, - stableCoins.dai.address, - 100, - ); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - - it('should fail: if redeem daily limit exceeded', async () => { - const { - redemptionVault, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(mTBILL, owner, 100_000); - await setInstantDailyLimitTest({ vault: redemptionVault, owner }, 1000); - - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed limit', - }, - ); - }); - - it('should fail: if min receive amount greater then actual', async () => { - const { - redemptionVault, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(mTBILL, owner, 100_000); - - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await redeemInstantTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - minAmount: parseUnits('1000000'), - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'RV: minReceiveAmount > actual', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 10000, - true, - ); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'RV: amountMTokenIn < fee', - }, - ); - - await removePaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: redemptionVault, owner }, 10000); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { revertMessage: 'RV: amountMTokenIn < fee' }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist ', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVault.setGreenlistEnable(true); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist ', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: when function with custom recipient is paused', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVault, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemInstant(address,uint256,uint256,address)', - ); - await pauseVaultFn(redemptionVault, selector); - await redeemInstantTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - } = await loadFixture(defaultDeploy); - - await redemptionVault.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await redeemInstantTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await redeemInstantTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await redeemInstantTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: user try to instant redeem fiat', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - await redemptionVault.MANUAL_FULLFILMENT_TOKEN(), - 100, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('redeem 100 mTBILL, greenlist enabled and user in greenlist ', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVault.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when 10% growth is applied', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customFeedGrowth, - } = await loadFixture(defaultDeploy); - - await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); - await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, 10); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - expectedAmountOut: parseUnits('99.000314820', 9), - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when -10% growth is applied', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customFeedGrowth, - } = await loadFixture(defaultDeploy); - - await setMinGrowthApr({ owner, customFeedGrowth }, -10); - await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); - await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, -10); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - expectedAmountOut: parseUnits('98.999685180', 9), - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - }); - - it('redeem 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - }); - - it('redeem 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redemptionVault.freeFromMinAmount(owner.address, true); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - }); - - it('redeem 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await addWaivedFeeAccountTest( - { vault: redemptionVault, owner }, - owner.address, - ); - await redeemInstantTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - waivedFee: true, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem 100 mTBILL (custom recipient overload)', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when recipient == msg.sender (custom recipient overload)', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[0], - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when other fn overload is paused (custom recipient overload)', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVault, - encodeFnSelector('redeemInstant(address,uint256,uint256)'), - ); - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when other fn overload is paused', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVault, - encodeFnSelector('redeemInstant(address,uint256,uint256,address)'), - ); - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('with permissioned mToken - burns/transfers mToken from greenlisted user and fee recipient', async () => { - const { - owner, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - mTokenPermissioned, - mTokenPermissionedRoles, - accessControl, - mTokenPermissionedRedemptionVault, - } = await loadFixture(mTokenPermissionedFixture); - - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - owner.address, - ); - - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - await mTokenPermissionedRedemptionVault.feeReceiver(), - ); - await mintToken(mTokenPermissioned, owner, 100_000); - await setInstantFeeTest( - { vault: mTokenPermissionedRedemptionVault, owner }, - 1000, - ); - await approveBase18( - owner, - mTokenPermissioned, - mTokenPermissionedRedemptionVault, - 100_000, - ); - - await mintToken( - stableCoins.dai, - mTokenPermissionedRedemptionVault, - 100_000, - ); - await addPaymentTokenTest( - { vault: mTokenPermissionedRedemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - await redeemInstantTest( - { - redemptionVault: mTokenPermissionedRedemptionVault, - owner, - mTBILL: mTokenPermissioned, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 999, - ); - }); - - it('with permissioned mToken - instant fee is 0, burns/transfers mToken from non-greenlisted user', async () => { - const { - owner, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - mTokenPermissioned, - mTokenPermissionedRoles, - accessControl, - mTokenPermissionedRedemptionVault, - } = await loadFixture(mTokenPermissionedFixture); - - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - owner.address, - ); - await mintToken(mTokenPermissioned, owner, 100_000); - await accessControl.revokeRole( - mTokenPermissionedRoles.greenlisted, - owner.address, - ); - await setInstantFeeTest( - { vault: mTokenPermissionedRedemptionVault, owner }, - 0, - ); - await approveBase18( - owner, - mTokenPermissioned, - mTokenPermissionedRedemptionVault, - 100_000, - ); - - await mintToken( - stableCoins.dai, - mTokenPermissionedRedemptionVault, - 100_000, - ); - await addPaymentTokenTest( - { vault: mTokenPermissionedRedemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - await redeemInstantTest( - { - redemptionVault: mTokenPermissionedRedemptionVault, - owner, - mTBILL: mTokenPermissioned, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 999, - ); - }); - - it('should fail: with permissioned mToken - burns/transfers mToken from greenlisted user but fee recipient is not greenlisted', async () => { - const { - owner, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - mTokenPermissioned, - mTokenPermissionedRoles, - accessControl, - mTokenPermissionedRedemptionVault, - } = await loadFixture(mTokenPermissionedFixture); - - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - owner.address, - ); - await mintToken(mTokenPermissioned, owner, 100_000); - await setInstantFeeTest( - { vault: mTokenPermissionedRedemptionVault, owner }, - 1000, - ); - await approveBase18( - owner, - mTokenPermissioned, - mTokenPermissionedRedemptionVault, - 100_000, - ); - - await mintToken( - stableCoins.dai, - mTokenPermissionedRedemptionVault, - 100_000, - ); - await addPaymentTokenTest( - { vault: mTokenPermissionedRedemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - await redeemInstantTest( - { - redemptionVault: mTokenPermissionedRedemptionVault, - owner, - mTBILL: mTokenPermissioned, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 999, - { - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('should fail: with permissioned mToken - redeem instant burns/transfers mToken from non-greenlisted user', async () => { - const { - owner, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - mTokenPermissioned, - mTokenPermissionedRedemptionVault, - mTokenPermissionedRoles, - accessControl, - } = await loadFixture(mTokenPermissionedFixture); - - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - owner.address, - ); - await mintToken(mTokenPermissioned, owner, 100_000); - await setInstantFeeTest( - { vault: mTokenPermissionedRedemptionVault, owner }, - 1000, - ); - await accessControl.revokeRole( - mTokenPermissionedRoles.greenlisted, - owner.address, - ); - await approveBase18( - owner, - mTokenPermissioned, - mTokenPermissionedRedemptionVault, - 100_000, - ); - - await mintToken( - stableCoins.dai, - mTokenPermissionedRedemptionVault, - 100_000, - ); - await addPaymentTokenTest( - { vault: mTokenPermissionedRedemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - await redeemInstantTest( - { - redemptionVault: mTokenPermissionedRedemptionVault, - owner, - mTBILL: mTokenPermissioned, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 999, - { - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - }); - - describe('redeemRequest()', () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 0, - { - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVault, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector('redeemRequest(address,uint256)'); - await pauseVaultFn(redemptionVault, selector); - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mockedAggregatorMToken, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, redemptionVault, 10); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(mTBILL, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 0); - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minAmount', async () => { - const { - redemptionVault, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 99_999, - { - revertMessage: 'RV: amount < min', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 10000, - true, - ); - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'RV: amountMTokenIn < fee', - }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist ', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVault.setGreenlistEnable(true); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist ', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: when function paused (custom recipient overload)', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVault, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemRequest(address,uint256,address)', - ); - await pauseVaultFn(redemptionVault, selector); - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - } = await loadFixture(defaultDeploy); - - await redemptionVault.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: user try to redeem fiat in basic request (custom recipient overload)', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - await redemptionVault.MANUAL_FULLFILMENT_TOKEN(), - 100, - { - revertMessage: 'RV: tokenOut == fiat', - }, - ); - }); - - it('should fail: user try to redeem fiat in basic request', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - await redemptionVault.MANUAL_FULLFILMENT_TOKEN(), - 100, - { - revertMessage: 'RV: tokenOut == fiat', - }, - ); - }); - - it('redeem request 100 mTBILL, greenlist enabled and user in greenlist ', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVault.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem request with 10% growth is applied', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customFeedGrowth, - } = await loadFixture(defaultDeploy); - - await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); - await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, 10); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem request with -10% growth is applied', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customFeedGrowth, - } = await loadFixture(defaultDeploy); - - await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); - await setMinGrowthApr({ owner, customFeedGrowth }, -10); - await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, -10); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - }); - - it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - }); - - it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redemptionVault.freeFromMinAmount(owner.address, true); - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - }); - - it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await addWaivedFeeAccountTest( - { vault: redemptionVault, owner }, - owner.address, - ); - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - waivedFee: true, - }, - stableCoins.dai, - 100, - ); - }); - it('redeem request 100 mTBILL (custom recipient overload)', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem request 100 mTBILL when recipient == msg.sender (custom recipient overload)', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[0], - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem request 100 mTBILL when other fn overload is paused (custom recipient overload)', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVault, - encodeFnSelector('redeemRequest(address,uint256)'), - ); - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem request 100 mTBILL when other fn overload is paused', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVault, - encodeFnSelector('redeemRequest(address,uint256,address)'), - ); - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - }); - - describe('redeemFiatRequest()', () => { - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - { - from: regularAccounts[0], - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVault, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector('redeemFiatRequest(uint256)'); - await pauseVaultFn(redemptionVault, selector); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await approveBase18(owner, mTBILL, redemptionVault, 100); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - redemptionVault, - mTBILL, - mockedAggregatorMToken, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100_000); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 0); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 10, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minFiatRedeemAmount', async () => { - const { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100_000); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setMinFiatRedeemAmountTest({ redemptionVault, owner }, 100_000); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 99_999, - { - revertMessage: 'RV: amount < min', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await setFiatAdditionalFeeTest({ redemptionVault, owner }, 10000); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - { - revertMessage: 'RV: amountMTokenIn < fee', - }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist ', async () => { - const { owner, redemptionVault, mTBILL, mTokenToUsdDataFeed } = - await loadFixture(defaultDeploy); - - await redemptionVault.setGreenlistEnable(true); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist ', async () => { - const { - owner, - redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - regularAccounts, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('redeem fiat request 100 mTBILL, greenlist enabled and user in greenlist ', async () => { - const { - owner, - redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await redemptionVault.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem fiat request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - ); - }); - - it('redeem fiat request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - ); - }); - - it('redeem fiat request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redemptionVault.freeFromMinAmount(owner.address, true); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - ); - }); - - it('redeem fiat request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await addWaivedFeeAccountTest( - { vault: redemptionVault, owner }, - owner.address, - ); - await redeemFiatRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - waivedFee: true, - }, - 100, - ); - }); - }); - - describe('approveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = - await loadFixture(defaultDeploy); - await approveRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - parseUnits('1'), - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - }); - }); - - describe('approveRequest() with fiat', async () => { - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - ); - const requestId = 0; - - await changeTokenAllowanceTest( - { vault: redemptionVault, owner }, - constants.AddressZero, - parseUnits('100'), - ); - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - }); - }); - - describe('safeApproveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = - await loadFixture(defaultDeploy); - await safeApproveRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - parseUnits('1'), - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: if new rate greater then variabilityTolerance', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('6'), - { revertMessage: 'MV: exceed price diviation' }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.000001'), - ); - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.00001'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('safe approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.000001'), - ); - }); - }); - - describe('safeBulkApproveRequestAtSavedRate()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = - await loadFixture(defaultDeploy); - await safeBulkApproveRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }], - 'request-rate', - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }], - 'request-rate', - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - 'request-rate', - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - 'request-rate', - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when one of them already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - parseUnits('5.000001'), - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0 }], - 'request-rate', - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when couple of them have equal id', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - parseUnits('5.000001'), - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 1 }], - 'request-rate', - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('approve 1 request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - 'request-rate', - ); - }); - - it('approve 2 request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0 }], - 'request-rate', - ); - }); - - it('approve 10 request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVault, 1000); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - for (let i = 0; i < 10; i++) { - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[i], - }, - stableCoins.dai, - 100, - ); - } - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - Array.from({ length: 10 }, (_, i) => ({ id: i })), - 'request-rate', - ); - }); - - it('approve 1 request when there is not enough liquidity', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId, expectedToExecute: false }], - 'request-rate', - ); - }); - - it('approve 2 request when there is enough liquidity only for first one', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 600); - - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 600, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [ - { id: 0, expectedToExecute: true }, - { id: 1, expectedToExecute: false }, - ], - 'request-rate', - ); - }); - - it('approve 2 requests both with different payment tokens', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await mintToken(stableCoins.usdc, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await approveBase18( - requestRedeemer, - stableCoins.usdc, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 100, - ); - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0 }], - 'request-rate', - ); - }); - - it('approve 2 requests both with different payment tokens when there is not enough liquidity for first one', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, requestRedeemer, 100000); - - await approveBase18( - requestRedeemer, - stableCoins.usdc, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 100, - ); - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0, expectedToExecute: false }], - 'request-rate', - ); - }); - }); - - describe('safeBulkApproveRequest() (custom price overload)', async () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = - await loadFixture(defaultDeploy); - await safeBulkApproveRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }], - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }], - parseUnits('1'), - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: if new rate greater then variabilityTolerance', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - parseUnits('6'), - { revertMessage: 'MV: exceed price diviation' }, - ); - }); - - it('should fail: if new rate lower then variabilityTolerance', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - parseUnits('4'), - { revertMessage: 'MV: exceed price diviation' }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - parseUnits('5.000001'), - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - parseUnits('5.00001'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when one of them already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - parseUnits('5.000001'), - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0 }], - parseUnits('5.00001'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when couple of them have equal id', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - parseUnits('5.000001'), - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 1 }], - parseUnits('5.00001'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('approve 1 request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - parseUnits('5.000001'), - ); - }); - - it('approve 2 request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0 }], - parseUnits('5.000001'), - ); - }); - - it('approve 10 request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVault, 1000); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - for (let i = 0; i < 10; i++) { - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[i], - }, - stableCoins.dai, - 100, - ); - } - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - Array.from({ length: 10 }, (_, i) => ({ id: i })), - parseUnits('5.000001'), - ); - }); - - it('approve 1 request when there is not enough liquidity', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId, expectedToExecute: false }], - parseUnits('5.000001'), - ); - }); - - it('approve 2 request when there is enough liquidity only for first one', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 600); - - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 600, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [ - { id: 0, expectedToExecute: true }, - { id: 1, expectedToExecute: false }, - ], - parseUnits('5.000001'), - ); - }); - - it('approve 2 requests both with different payment tokens', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await mintToken(stableCoins.usdc, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await approveBase18( - requestRedeemer, - stableCoins.usdc, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 100, - ); - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0 }], - parseUnits('5.000001'), - ); - }); - - it('approve 2 requests both with different payment tokens when there is not enough liquidity for first one', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, requestRedeemer, 100000); - - await approveBase18( - requestRedeemer, - stableCoins.usdc, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 100, - ); - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0, expectedToExecute: false }], - parseUnits('5.000001'), - ); - }); - }); - - describe('safeBulkApproveRequest() (current price overload)', async () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = - await loadFixture(defaultDeploy); - await safeBulkApproveRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - [{ id: 1 }], - undefined, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }], - undefined, - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: if new rate greater then variabilityTolerance', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 6); - - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - undefined, - { revertMessage: 'MV: exceed price diviation' }, - ); - }); - - it('should fail: if new rate lower then variabilityTolerance', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 4); - - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - undefined, - { revertMessage: 'MV: exceed price diviation' }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - undefined, - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - undefined, - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when one of them already precessed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - parseUnits('5.000001'), - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0 }], - undefined, - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('should fail: process multiple requests, when couple of them have equal id', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - parseUnits('5.000001'), - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 1 }], - undefined, - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('approve 1 request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - undefined, - ); - }); - - it('approve 1 request from vaut admin account when 10% growth is applied', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - customFeedGrowth, - } = await loadFixture(defaultDeploy); - - await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); - await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, 10); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - undefined, - ); - }); - - it('approve 1 request from vaut admin account when -10% growth is applied', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - customFeedGrowth, - } = await loadFixture(defaultDeploy); - - await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); - await setMinGrowthApr({ owner, customFeedGrowth }, -10); - await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, -10); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId }], - undefined, - ); - }); - - it('approve 2 request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0 }], - undefined, - ); - }); - - it('approve 10 request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVault, 1000); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - for (let i = 0; i < 10; i++) { - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[i], - }, - stableCoins.dai, - 100, - ); - } - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - Array.from({ length: 10 }, (_, i) => ({ id: i })), - undefined, - ); - }); - - it('approve 1 request when there is not enough liquidity', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: requestId, expectedToExecute: false }], - undefined, - ); - }); - - it('approve 2 request when there is enough liquidity only for first one', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 600); - - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 600, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [ - { id: 0, expectedToExecute: true }, - { id: 1, expectedToExecute: false }, - ], - undefined, - ); - }); - - it('approve 2 requests both with different payment tokens', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await mintToken(stableCoins.usdc, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await approveBase18( - requestRedeemer, - stableCoins.usdc, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 100, - ); - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0 }], - undefined, - ); - }); - - it('approve 2 requests both with different payment tokens when there is not enough liquidity for first one', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, requestRedeemer, 100000); - - await approveBase18( - requestRedeemer, - stableCoins.usdc, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 200); - await approveBase18(owner, mTBILL, redemptionVault, 200); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 100, - ); - - await safeBulkApproveRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - [{ id: 1 }, { id: 0, expectedToExecute: false }], - undefined, - ); - }); - }); - - describe('rejectRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = - await loadFixture(defaultDeploy); - await rejectRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - ); - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('safe approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - ); - }); - }); - - describe('redeemInstant() complex', () => { - it('should fail: when is paused', async () => { - const { - redemptionVault, - owner, - mTBILL, - stableCoins, - regularAccounts, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(redemptionVault); - await mintToken(stableCoins.dai, redemptionVault, 100); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVault, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('is on pause, but admin can use everything', async () => { - const { - redemptionVault, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(redemptionVault); - - await mintToken(stableCoins.dai, redemptionVault, 100); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, stableCoins.dai, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('call for amount == minAmount', async () => { - const { - redemptionVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVault, 100_000); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100_000, - ); - }); - - it('redeem 100 mtbill, when price is 5$, 125 mtbill when price is 5.1$, 114 mtbill when price is 5.4$', async () => { - const { - owner, - mockedAggregator, - redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await mintToken(mTBILL, owner, 125); - await mintToken(mTBILL, owner, 114); - - await mintToken(stableCoins.dai, redemptionVault, 1000); - await mintToken(stableCoins.usdc, redemptionVault, 1250); - await mintToken(stableCoins.usdt, redemptionVault, 1140); - - await approveBase18(owner, mTBILL, redemptionVault, 100 + 125 + 114); - - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setRoundData({ mockedAggregator }, 1.04); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - - await setRoundData({ mockedAggregator }, 1); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 125, - ); - - await setRoundData({ mockedAggregator }, 1.01); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdt, - 114, - ); - }); - }); - - describe('redeemRequest() complex', () => { - it('should fail: when is paused', async () => { - const { - redemptionVault, - owner, - mTBILL, - stableCoins, - regularAccounts, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(redemptionVault); - await mintToken(stableCoins.dai, redemptionVault, 100); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVault, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('is on pause, but admin can use everything', async () => { - const { - redemptionVault, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(redemptionVault); - - await mintToken(stableCoins.dai, redemptionVault, 1000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, stableCoins.dai, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('call for amount == minAmount, then approve', async () => { - const { - redemptionVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100_000, - ); - - const requestId = 0; - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - }); - - it('call for amount == minAmount, then safe approve', async () => { - const { - redemptionVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await mintToken(stableCoins.dai, requestRedeemer, 1000000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 1000000, - ); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100_000, - ); - - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1.000001'), - ); - }); - - it('call for amount == minAmount, then reject', async () => { - const { - redemptionVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVault, 100_000); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100_000, - ); - - const requestId = 0; - - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - ); - }); - }); - - describe('_convertUsdToToken', () => { - it('should fail: when amountUsd == 0', async () => { - const { redemptionVault } = await loadFixture(defaultDeploy); - - await expect( - redemptionVault.convertUsdToTokenTest(0, constants.AddressZero), - ).revertedWith('RV: amount zero'); - }); - - it('should fail: when tokenRate == 0', async () => { - const { redemptionVault } = await loadFixture(defaultDeploy); - - await redemptionVault.setOverrideGetTokenRate(true); - await redemptionVault.setGetTokenRateValue(0); - - await expect( - redemptionVault.convertUsdToTokenTest(1, constants.AddressZero), - ).revertedWith('RV: rate zero'); - }); - }); - - describe('_convertMTokenToUsd', () => { - it('should fail: when amountMToken == 0', async () => { - const { redemptionVault } = await loadFixture(defaultDeploy); - - await expect(redemptionVault.convertMTokenToUsdTest(0)).revertedWith( - 'RV: amount zero', - ); - }); - - it('should fail: when amountMToken == 0', async () => { - const { redemptionVault } = await loadFixture(defaultDeploy); - - await redemptionVault.setOverrideGetTokenRate(true); - await redemptionVault.setGetTokenRateValue(0); - - await expect(redemptionVault.convertMTokenToUsdTest(1)).revertedWith( - 'RV: rate zero', - ); - }); - }); - - describe('_calcAndValidateRedeem', () => { - it('should fail: when tokenOut is not MANUAL_FULLFILMENT_TOKEN but isFiat = true', async () => { - const { redemptionVault, stableCoins } = await loadFixture(defaultDeploy); - - await expect( - redemptionVault.calcAndValidateRedeemTest( - constants.AddressZero, - stableCoins.dai.address, - parseUnits('100'), - false, - true, - ), - ).revertedWith('RV: tokenOut != fiat'); - }); - }); -}); + }, +); diff --git a/test/unit/RedemptionVaultWithAave.test.ts b/test/unit/RedemptionVaultWithAave.test.ts index 72869bae..024350de 100644 --- a/test/unit/RedemptionVaultWithAave.test.ts +++ b/test/unit/RedemptionVaultWithAave.test.ts @@ -1,2703 +1,1180 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; import { constants } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; +import { + baseInitParamsRv, + redemptionVaultSuits, +} from './suits/redemption-vault.suits'; + import { encodeFnSelector } from '../../helpers/utils'; import { - ManageableVaultTester__factory, + RedemptionVaultWithAave__factory, RedemptionVaultWithAaveTest__factory, } from '../../typechain-types'; -import { acErrors, blackList, greenList } from '../common/ac.helpers'; +import { acErrors } from '../common/ac.helpers'; import { approveBase18, mintToken, pauseVaultFn, + validateImplementation, } from '../common/common.helpers'; import { setRoundData } from '../common/data-feed.helpers'; import { defaultDeploy } from '../common/fixtures'; import { addPaymentTokenTest, setInstantFeeTest, - setMinAmountTest, - setInstantDailyLimitTest, - addWaivedFeeAccountTest, - removeWaivedFeeAccountTest, - setVariabilityToleranceTest, - removePaymentTokenTest, - withdrawTest, - changeTokenFeeTest, - changeTokenAllowanceTest, + setWaivedFeeAccountTest, } from '../common/manageable-vault.helpers'; import { - approveRedeemRequestTest, - redeemFiatRequestTest, + removeAavePoolTest, + setAavePoolTest, +} from '../common/redemption-vault-aave.helpers'; +import { redeemInstantTest, - redeemRequestTest, - rejectRedeemRequestTest, - safeApproveRedeemRequestTest, - setFiatAdditionalFeeTest, - setMinFiatRedeemAmountTest, + setPreferLoanLiquidityTest, + setLoanLpTest, } from '../common/redemption-vault.helpers'; -import { sanctionUser } from '../common/with-sanctions-list.helpers'; - -describe('RedemptionVaultWithAave', function () { - it('deployment', async () => { - const { - redemptionVaultWithAave, - aavePoolMock, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - stableCoins, - roles, - } = await loadFixture(defaultDeploy); - - expect(await redemptionVaultWithAave.mToken()).eq(mTBILL.address); - - expect(await redemptionVaultWithAave.ONE_HUNDRED_PERCENT()).eq('10000'); - - expect(await redemptionVaultWithAave.paused()).eq(false); - - expect(await redemptionVaultWithAave.tokensReceiver()).eq( - tokensReceiver.address, - ); - expect(await redemptionVaultWithAave.feeReceiver()).eq(feeReceiver.address); - - expect(await redemptionVaultWithAave.minAmount()).eq(1000); - expect(await redemptionVaultWithAave.minFiatRedeemAmount()).eq(1000); - - expect(await redemptionVaultWithAave.instantFee()).eq('100'); - - expect(await redemptionVaultWithAave.instantDailyLimit()).eq( - parseUnits('100000'), - ); - - expect(await redemptionVaultWithAave.mTokenDataFeed()).eq( - mTokenToUsdDataFeed.address, - ); - expect(await redemptionVaultWithAave.variationTolerance()).eq(1); - - expect(await redemptionVaultWithAave.vaultRole()).eq( - roles.tokenRoles.mTBILL.redemptionVaultAdmin, - ); - - expect(await redemptionVaultWithAave.MANUAL_FULLFILMENT_TOKEN()).eq( - ethers.constants.AddressZero, - ); - +import { initializeRvWithAave } from '../common/vault-initializer.helpers'; + +redemptionVaultSuits( + 'RedemptionVaultWithAave', + defaultDeploy, + { + createNew: async (owner: SignerWithAddress) => + new RedemptionVaultWithAaveTest__factory(owner).deploy(), + key: 'redemptionVaultWithAave', + }, + async (fixture) => { + const { redemptionVaultWithAave, stableCoins, aavePoolMock } = fixture; expect( await redemptionVaultWithAave.aavePools(stableCoins.usdc.address), ).eq(aavePoolMock.address); - }); - - it('failing deployment', async () => { - const { - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - accessControl, - mockedSanctionsList, - owner, - } = await loadFixture(defaultDeploy); - - const redemptionVaultWithAave = - await new RedemptionVaultWithAaveTest__factory(owner).deploy(); - - await expect( - redemptionVaultWithAave.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - { - fiatAdditionalFee: 10000, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: parseUnits('100'), - }, - constants.AddressZero, - ), - ).to.be.reverted; - }); - - describe('initialization', () => { - it('should fail: call initialize() when already initialized', async () => { - const { redemptionVaultWithAave } = await loadFixture(defaultDeploy); - - await expect( - redemptionVaultWithAave.initialize( - constants.AddressZero, - { - mToken: constants.AddressZero, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: constants.AddressZero, - tokensReceiver: constants.AddressZero, - }, - { - instantFee: 0, - instantDailyLimit: 0, - }, - constants.AddressZero, - 0, - 0, - { - fiatAdditionalFee: 0, - fiatFlatFee: 0, - minFiatRedeemAmount: 0, - }, - constants.AddressZero, - ), - ).revertedWith('Initializable: contract is already initialized'); - }); - - it('should fail: call with initializing == false', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initializeWithoutInitializer( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('Initializable: contract is not initializing'); - }); - }); - - describe('setAavePool()', () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithAave, - regularAccounts, - stableCoins, - aavePoolMock, - } = await loadFixture(defaultDeploy); - await expect( - redemptionVaultWithAave - .connect(regularAccounts[0]) - .setAavePool(stableCoins.usdc.address, aavePoolMock.address), - ).to.be.revertedWith('WMAC: hasnt role'); - }); - - it('should fail: zero address', async () => { - const { redemptionVaultWithAave, stableCoins } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithAave.setAavePool( - stableCoins.usdc.address, - constants.AddressZero, - ), - ).to.be.revertedWith('zero address'); - }); - - it('should succeed and emit SetAavePool event', async () => { - const { redemptionVaultWithAave, owner, stableCoins, aavePoolMock } = - await loadFixture(defaultDeploy); - - await expect( - redemptionVaultWithAave.setAavePool( - stableCoins.usdc.address, - aavePoolMock.address, - ), - ) - .to.emit(redemptionVaultWithAave, 'SetAavePool') - .withArgs( - owner.address, - stableCoins.usdc.address, - aavePoolMock.address, - ); - - expect( - await redemptionVaultWithAave.aavePools(stableCoins.usdc.address), - ).eq(aavePoolMock.address); - }); - }); - - describe('removeAavePool()', () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithAave, regularAccounts, stableCoins } = - await loadFixture(defaultDeploy); - await expect( - redemptionVaultWithAave - .connect(regularAccounts[0]) - .removeAavePool(stableCoins.usdc.address), - ).to.be.revertedWith('WMAC: hasnt role'); - }); - - it('should fail: pool not set', async () => { - const { redemptionVaultWithAave, stableCoins } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithAave.removeAavePool(stableCoins.dai.address), - ).to.be.revertedWith('RVA: pool not set'); - }); - - it('should succeed and emit RemoveAavePool event', async () => { - const { redemptionVaultWithAave, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - - await expect( - redemptionVaultWithAave.removeAavePool(stableCoins.usdc.address), - ) - .to.emit(redemptionVaultWithAave, 'RemoveAavePool') - .withArgs(owner.address, stableCoins.usdc.address); - - expect( - await redemptionVaultWithAave.aavePools(stableCoins.usdc.address), - ).eq(constants.AddressZero); - }); - }); - - describe('setMinAmount()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithAave, regularAccounts } = - await loadFixture(defaultDeploy); - - await setMinAmountTest({ vault: redemptionVaultWithAave, owner }, 1.1, { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, + await validateImplementation(RedemptionVaultWithAave__factory); + }, + { + deployUninitialized: (fixture) => + new RedemptionVaultWithAaveTest__factory(fixture.owner).deploy(), + initialize: async (fixture, params, opt) => { + await initializeRvWithAave( + { ...baseInitParamsRv(fixture), ...params }, + opt?.contract, + opt, + ); + }, + }, + (defaultDeploy) => { + describe('RedemptionVaultWithAave', () => { + describe('setAavePool()', () => { + it('should fail: call from address without vault admin role', async () => { + const { + redemptionVaultWithAave, + regularAccounts, + stableCoins, + aavePoolMock, + } = await loadFixture(defaultDeploy); + await expect( + redemptionVaultWithAave + .connect(regularAccounts[0]) + .setAavePool(stableCoins.usdc.address, aavePoolMock.address), + ).to.be.revertedWithCustomError( + redemptionVaultWithAave, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + + it('should fail: zero address', async () => { + const { redemptionVaultWithAave, stableCoins } = await loadFixture( + defaultDeploy, + ); + await expect( + redemptionVaultWithAave.setAavePool( + stableCoins.usdc.address, + constants.AddressZero, + ), + ).to.be.revertedWithCustomError( + redemptionVaultWithAave, + 'InvalidAddress', + ); + }); + + it('should fail: when function is paused', async () => { + const { + redemptionVaultWithAave, + owner, + stableCoins, + aavePoolMock, + pauseManager, + } = await loadFixture(defaultDeploy); + await pauseVaultFn( + { pauseManager, owner }, + redemptionVaultWithAave, + encodeFnSelector('setAavePool(address,address)'), + ); + await setAavePoolTest( + { redemptionVault: redemptionVaultWithAave, owner }, + stableCoins.usdc.address, + aavePoolMock.address, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should succeed and emit SetAavePool event', async () => { + const { redemptionVaultWithAave, owner, stableCoins, aavePoolMock } = + await loadFixture(defaultDeploy); + + await expect( + redemptionVaultWithAave.setAavePool( + stableCoins.usdc.address, + aavePoolMock.address, + ), + ) + .to.emit(redemptionVaultWithAave, 'SetAavePool') + .withArgs(stableCoins.usdc.address, aavePoolMock.address); + + expect( + await redemptionVaultWithAave.aavePools(stableCoins.usdc.address), + ).eq(aavePoolMock.address); + }); }); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithAave } = await loadFixture( - defaultDeploy, - ); - await setMinAmountTest({ vault: redemptionVaultWithAave, owner }, 1.1); - }); - }); - - describe('setMinFiatRedeemAmount()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithAave, regularAccounts } = - await loadFixture(defaultDeploy); - - await setMinFiatRedeemAmountTest( - { redemptionVault: redemptionVaultWithAave, owner }, - 1.1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithAave } = await loadFixture( - defaultDeploy, - ); - await setMinFiatRedeemAmountTest( - { redemptionVault: redemptionVaultWithAave, owner }, - 1.1, - ); - }); - }); - - describe('setFiatAdditionalFee()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithAave, regularAccounts } = - await loadFixture(defaultDeploy); - - await setFiatAdditionalFeeTest( - { redemptionVault: redemptionVaultWithAave, owner }, - 100, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithAave } = await loadFixture( - defaultDeploy, - ); - await setFiatAdditionalFeeTest( - { redemptionVault: redemptionVaultWithAave, owner }, - 100, - ); - }); - }); - - describe('setInstantDailyLimit()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithAave, regularAccounts } = - await loadFixture(defaultDeploy); - - await setInstantDailyLimitTest( - { vault: redemptionVaultWithAave, owner }, - parseUnits('1000'), - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('should fail: try to set 0 limit', async () => { - const { owner, redemptionVaultWithAave } = await loadFixture( - defaultDeploy, - ); - - await setInstantDailyLimitTest( - { vault: redemptionVaultWithAave, owner }, - constants.Zero, - { - revertMessage: 'MV: limit zero', - }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithAave } = await loadFixture( - defaultDeploy, - ); - await setInstantDailyLimitTest( - { vault: redemptionVaultWithAave, owner }, - parseUnits('1000'), - ); - }); - }); - - describe('addPaymentToken()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithAave, regularAccounts } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - ethers.constants.AddressZero, - ethers.constants.AddressZero, - 0, - true, - constants.MaxUint256, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithAave, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - }); - }); - - describe('removePaymentToken()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithAave, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await removePaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - { - from: (await ethers.getSigners())[10], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithAave, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await removePaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - ); - }); - }); - - describe('addWaivedFeeAccount()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithAave, regularAccounts } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithAave, owner }, - regularAccounts[0].address, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithAave, regularAccounts } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithAave, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('removeWaivedFeeAccount()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithAave, regularAccounts } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithAave, owner }, - regularAccounts[0].address, - ); - await removeWaivedFeeAccountTest( - { vault: redemptionVaultWithAave, owner }, - regularAccounts[0].address, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithAave, regularAccounts } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithAave, owner }, - regularAccounts[0].address, - ); - await removeWaivedFeeAccountTest( - { vault: redemptionVaultWithAave, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('setFee()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithAave, regularAccounts } = - await loadFixture(defaultDeploy); - await setInstantFeeTest({ vault: redemptionVaultWithAave, owner }, 100, { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, + describe('removeAavePool()', () => { + it('should fail: call from address without vault admin role', async () => { + const { redemptionVaultWithAave, regularAccounts, stableCoins } = + await loadFixture(defaultDeploy); + await expect( + redemptionVaultWithAave + .connect(regularAccounts[0]) + .removeAavePool(stableCoins.usdc.address), + ).to.be.revertedWithCustomError( + redemptionVaultWithAave, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); + + it('should fail: pool not set', async () => { + const { redemptionVaultWithAave, stableCoins } = await loadFixture( + defaultDeploy, + ); + await expect( + redemptionVaultWithAave.removeAavePool(stableCoins.dai.address), + ).to.be.revertedWithCustomError( + redemptionVaultWithAave, + 'PoolNotSet', + ); + }); + + it('should fail: when function is paused', async () => { + const { redemptionVaultWithAave, owner, stableCoins, pauseManager } = + await loadFixture(defaultDeploy); + await pauseVaultFn( + { pauseManager, owner }, + redemptionVaultWithAave, + encodeFnSelector('removeAavePool(address)'), + ); + await removeAavePoolTest( + { redemptionVault: redemptionVaultWithAave, owner }, + stableCoins.usdc.address, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should succeed and emit RemoveAavePool event', async () => { + const { redemptionVaultWithAave, owner, stableCoins } = + await loadFixture(defaultDeploy); + + await expect( + redemptionVaultWithAave.removeAavePool(stableCoins.usdc.address), + ) + .to.emit(redemptionVaultWithAave, 'RemoveAavePool') + .withArgs(stableCoins.usdc.address); + + expect( + await redemptionVaultWithAave.aavePools(stableCoins.usdc.address), + ).eq(constants.AddressZero); + }); }); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithAave } = await loadFixture( - defaultDeploy, - ); - await setInstantFeeTest({ vault: redemptionVaultWithAave, owner }, 100); - }); - }); - - describe('setVariabilityTolerance()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithAave, regularAccounts } = - await loadFixture(defaultDeploy); - await setVariabilityToleranceTest( - { vault: redemptionVaultWithAave, owner }, - 100, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithAave } = await loadFixture( - defaultDeploy, - ); - await setVariabilityToleranceTest( - { vault: redemptionVaultWithAave, owner }, - 100, - ); - }); - }); - - describe('withdrawToken()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithAave, stableCoins, regularAccounts } = - await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, redemptionVaultWithAave, 1); - await withdrawTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - 1, - regularAccounts[0], - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithAave, stableCoins, regularAccounts } = - await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, redemptionVaultWithAave, 1); - await withdrawTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - 1, - regularAccounts[0], - ); - }); - }); - - describe('freeFromMinAmount()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithAave, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithAave - .connect(regularAccounts[0]) - .freeFromMinAmount(regularAccounts[1].address, true), - ).to.be.revertedWith('WMAC: hasnt role'); - }); - it('should not fail', async () => { - const { redemptionVaultWithAave, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithAave.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.not.reverted; - - expect( - await redemptionVaultWithAave.isFreeFromMinAmount( - regularAccounts[0].address, - ), - ).to.eq(true); - }); - }); - - describe('changeTokenAllowance()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithAave, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai.address, - 100, - { - from: (await ethers.getSigners())[10], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithAave, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai.address, - 100, - ); - }); - }); - - describe('changeTokenFee()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithAave, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai.address, - 100, - { - from: (await ethers.getSigners())[10], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithAave, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai.address, - 100, - ); - }); - }); - - describe('checkAndRedeemAave()', () => { - it('should not withdraw from Aave when contract has enough balance', async () => { - const { redemptionVaultWithAave, stableCoins, aUSDC } = await loadFixture( - defaultDeploy, - ); - - const usdcAmount = parseUnits('1000', 8); - await stableCoins.usdc.mint(redemptionVaultWithAave.address, usdcAmount); - - const balanceBefore = await stableCoins.usdc.balanceOf( - redemptionVaultWithAave.address, - ); - const aTokenBefore = await aUSDC.balanceOf( - redemptionVaultWithAave.address, - ); - - await redemptionVaultWithAave.checkAndRedeemAave( - stableCoins.usdc.address, - parseUnits('500', 8), - ); - - const balanceAfter = await stableCoins.usdc.balanceOf( - redemptionVaultWithAave.address, - ); - const aTokenAfter = await aUSDC.balanceOf( - redemptionVaultWithAave.address, - ); - expect(balanceAfter).to.equal(balanceBefore); - expect(aTokenAfter).to.equal(aTokenBefore); - }); - - it('should withdraw missing amount from Aave', async () => { - const { redemptionVaultWithAave, stableCoins, aUSDC } = await loadFixture( - defaultDeploy, - ); - - // Vault has 500 USDC, needs 1000 - const initialUsdc = parseUnits('500', 8); - await stableCoins.usdc.mint(redemptionVaultWithAave.address, initialUsdc); - - // Vault has 600 aUSDC - const aTokenAmount = parseUnits('600', 8); - await aUSDC.mint(redemptionVaultWithAave.address, aTokenAmount); - - await redemptionVaultWithAave.checkAndRedeemAave( - stableCoins.usdc.address, - parseUnits('1000', 8), - ); - - // Vault should now have 1000 USDC (500 original + 500 withdrawn from Aave) - const usdcAfter = await stableCoins.usdc.balanceOf( - redemptionVaultWithAave.address, - ); - expect(usdcAfter).to.equal(parseUnits('1000', 8)); - - // aToken balance should decrease by 500 - const aTokenAfter = await aUSDC.balanceOf( - redemptionVaultWithAave.address, - ); - expect(aTokenAfter).to.equal(parseUnits('100', 8)); - }); - - it('should revert when no pool set for token', async () => { - const { redemptionVaultWithAave, stableCoins } = await loadFixture( - defaultDeploy, - ); - - await expect( - redemptionVaultWithAave.checkAndRedeemAave( - stableCoins.dai.address, - parseUnits('1000', 9), - ), - ).to.be.revertedWith('RVA: no pool for token'); - }); - - it('should revert when contract has insufficient aToken balance', async () => { - const { redemptionVaultWithAave, stableCoins, aUSDC } = await loadFixture( - defaultDeploy, - ); - - // Vault has 200 USDC, needs 1000 - await stableCoins.usdc.mint( - redemptionVaultWithAave.address, - parseUnits('200', 8), - ); - - // Vault has only 300 aUSDC (not enough for 800 missing) - await aUSDC.mint(redemptionVaultWithAave.address, parseUnits('300', 8)); - - await expect( - redemptionVaultWithAave.checkAndRedeemAave( - stableCoins.usdc.address, - parseUnits('1000', 8), - ), - ).to.be.revertedWith('RVA: insufficient aToken balance'); - }); - - it('should revert when Aave pool has insufficient underlying liquidity', async () => { - const { redemptionVaultWithAave, stableCoins, aUSDC, aavePoolMock } = - await loadFixture(defaultDeploy); - - // Vault needs to withdraw from Aave - await stableCoins.usdc.mint( - redemptionVaultWithAave.address, - parseUnits('200', 8), - ); - - // Vault has enough aTokens - await aUSDC.mint(redemptionVaultWithAave.address, parseUnits('1000', 8)); - - // Drain the pool's USDC - const poolBalance = await stableCoins.usdc.balanceOf( - aavePoolMock.address, - ); - await aavePoolMock.withdrawAdmin( - stableCoins.usdc.address, - ( - await ethers.getSigners() - )[10].address, - poolBalance, - ); - - await expect( - redemptionVaultWithAave.checkAndRedeemAave( - stableCoins.usdc.address, - parseUnits('1000', 8), - ), - ).to.be.revertedWith('AaveV3PoolMock: InsufficientLiquidity'); - }); - - it('should revert when Aave withdraws less than missing amount', async () => { - const { redemptionVaultWithAave, stableCoins, aUSDC, aavePoolMock } = - await loadFixture(defaultDeploy); - - // Vault has 200 USDC, needs 1000 - await stableCoins.usdc.mint( - redemptionVaultWithAave.address, - parseUnits('200', 8), - ); - - // Vault has enough aTokens to cover the gap - await aUSDC.mint(redemptionVaultWithAave.address, parseUnits('1000', 8)); - - // Simulate partial Aave withdrawal - await aavePoolMock.setWithdrawReturnBps(5000); - - await expect( - redemptionVaultWithAave.checkAndRedeemAave( - stableCoins.usdc.address, - parseUnits('1000', 8), - ), - ).to.be.revertedWith('RVA: withdrawn < needed'); - }); - }); - - describe('redeemInstant()', () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 0, - { - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemInstant(address,uint256,uint256)', - ); - await pauseVaultFn(redemptionVaultWithAave, selector); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'ERC20: burn amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mockedAggregatorMToken, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.usdc, redemptionVaultWithAave, 10); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await mintToken(mTBILL, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minAmount', async () => { - const { - redemptionVaultWithAave, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100_000); - - await setMinAmountTest( - { vault: redemptionVaultWithAave, owner }, - 100_000, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'RV: amount < min', - }, - ); - }); - - it('should fail: if exceeds token allowance', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(mTBILL, owner, 100_000); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc.address, - 100, - ); - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100_000); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - - it('should fail: if daily limit exceeded', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(mTBILL, owner, 100_000); - await setInstantDailyLimitTest( - { vault: redemptionVaultWithAave, owner }, - 1000, - ); - - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100_000); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'MV: exceed limit', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 10000, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'RV: amountMTokenIn < fee', - }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithAave.setGreenlistEnable(true); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedSanctionsList, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - // ── Happy path tests ───────────────────────────────────────────────── - - it('redeem 100 mTBILL when vault has enough USDC (no Aave needed)', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - aUSDC, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithAave, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - const aTokenBefore = await aUSDC.balanceOf( - redemptionVaultWithAave.address, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - // aToken balance should not change - const aTokenAfter = await aUSDC.balanceOf( - redemptionVaultWithAave.address, - ); - expect(aTokenAfter).to.equal(aTokenBefore); - }); - - it('redeem 1000 mTBILL when vault has no USDC but has aTokens', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - aUSDC, - } = await loadFixture(defaultDeploy); - - // Mint aTokens to vault (enough for redemption) - await aUSDC.mint(redemptionVaultWithAave.address, parseUnits('9900', 8)); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: redemptionVaultWithAave, owner }, 0); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - const aTokenBefore = await aUSDC.balanceOf( - redemptionVaultWithAave.address, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1000, - ); - - const aTokenAfter = await aUSDC.balanceOf( - redemptionVaultWithAave.address, - ); - - // aTokens should decrease - expect(aTokenAfter).to.be.lt(aTokenBefore); - }); - - it('redeem 1000 mTBILL when vault has 100 USDC and sufficient aTokens (partial Aave)', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - aUSDC, - } = await loadFixture(defaultDeploy); - - // Vault has 100 USDC + 9900 aTokens - await mintToken(stableCoins.usdc, redemptionVaultWithAave, 100); - await aUSDC.mint(redemptionVaultWithAave.address, parseUnits('9900', 8)); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1000, - ); - }); - - it('redeem 1000 mTBILL with different prices (stable 1.03$, mToken 5$) and partial Aave', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - aUSDC, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithAave, 100); - await aUSDC.mint(redemptionVaultWithAave.address, parseUnits('15000', 8)); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redemptionVaultWithAave.freeFromMinAmount(owner.address, true); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1000, - ); - }); - - it('redeem 1000 mTBILL with waived fee and Aave withdrawal', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - aUSDC, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithAave, 100); - await aUSDC.mint(redemptionVaultWithAave.address, parseUnits('15000', 8)); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithAave, owner }, - owner.address, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - waivedFee: true, - }, - stableCoins.usdc, - 1000, - ); - }); - - it('should fail: insufficient aToken balance during redeemInstant', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - aUSDC, - } = await loadFixture(defaultDeploy); - - // Vault has no USDC and only 10 aTokens (not enough for 1000 mTBILL redemption) - await aUSDC.mint(redemptionVaultWithAave.address, parseUnits('10', 8)); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: redemptionVaultWithAave, owner }, 0); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - await expect( - redemptionVaultWithAave['redeemInstant(address,uint256,uint256)']( - stableCoins.usdc.address, - parseUnits('1000'), - 0, - ), - ).to.be.revertedWith('RVA: insufficient aToken balance'); - }); - - it('should fail: Aave pool has insufficient liquidity during redeemInstant', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - aUSDC, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - // Vault has aTokens but pool has no liquidity - await aUSDC.mint(redemptionVaultWithAave.address, parseUnits('10000', 8)); - await mintToken(mTBILL, owner, 100000); - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100000); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: redemptionVaultWithAave, owner }, 0); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - // Drain the pool - const poolBalance = await stableCoins.usdc.balanceOf( - aavePoolMock.address, - ); - await aavePoolMock.withdrawAdmin( - stableCoins.usdc.address, - ( - await ethers.getSigners() - )[10].address, - poolBalance, - ); - - await expect( - redemptionVaultWithAave['redeemInstant(address,uint256,uint256)']( - stableCoins.usdc.address, - parseUnits('1000'), - 0, - ), - ).to.be.revertedWith('AaveV3PoolMock: InsufficientLiquidity'); - }); - - it('should fail: short Aave withdrawal during redeemInstant', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - aUSDC, - aavePoolMock, - } = await loadFixture(defaultDeploy); - - await aUSDC.mint(redemptionVaultWithAave.address, parseUnits('10000', 8)); - await mintToken(mTBILL, owner, 100000); - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100000); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: redemptionVaultWithAave, owner }, 0); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await aavePoolMock.setWithdrawReturnBps(5000); - - await expect( - redemptionVaultWithAave['redeemInstant(address,uint256,uint256)']( - stableCoins.usdc.address, - parseUnits('1000'), - 0, - ), - ).to.be.revertedWith('RVA: withdrawn < needed'); - }); - - // ── Custom recipient tests ─────────────────────────────────────────── - - it('redeem 100 mTBILL (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithAave, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when other fn overload is paused (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithAave, - encodeFnSelector('redeemInstant(address,uint256,uint256)'), - ); - await mintToken(stableCoins.usdc, redemptionVaultWithAave, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when other fn overload is paused', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithAave, - encodeFnSelector('redeemInstant(address,uint256,uint256,address)'), - ); - await mintToken(stableCoins.usdc, redemptionVaultWithAave, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('should fail: when function with custom recipient is paused', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemInstant(address,uint256,uint256,address)', - ); - await pauseVaultFn(redemptionVaultWithAave, selector); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithAave.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - }); - - describe('redeemRequest()', () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 0, - { - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithAave, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector('redeemRequest(address,uint256)'); - await pauseVaultFn(redemptionVaultWithAave, selector); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('redeem request: happy path', async () => { - const { - owner, - redemptionVaultWithAave, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - }); - }); - - describe('redeemFiatRequest()', () => { - it('should fail: when function paused', async () => { - const { - owner, - redemptionVaultWithAave, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100000); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithAave, - 100000, - ); - const selector = encodeFnSelector('redeemFiatRequest(uint256)'); - await pauseVaultFn(redemptionVaultWithAave, selector); - await redeemFiatRequestTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - 100000, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('redeem fiat request: happy path', async () => { - const { owner, redemptionVaultWithAave, mTBILL, mTokenToUsdDataFeed } = - await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100000); - await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100000); - await redeemFiatRequestTest( - { - redemptionVault: redemptionVaultWithAave, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - 100000, - ); - }); - }); - - describe('approveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithAave: redemptionVault, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await approveRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithAave: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - parseUnits('1'), - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - }); - }); - - describe('approveRequest() with fiat', async () => { - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - ); - const requestId = 0; - await changeTokenAllowanceTest( - { vault: redemptionVault, owner }, - constants.AddressZero, - parseUnits('100'), - ); - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - }); - }); - - describe('safeApproveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithAave: redemptionVault, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await safeApproveRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithAave: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - parseUnits('1'), - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: if new rate greater then variabilityTolerance', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('6'), - { revertMessage: 'MV: exceed price diviation' }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.000001'), - ); - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.00001'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('safe approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave: redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.000001'), - ); - }); - }); - - describe('rejectRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithAave: redemptionVault, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await rejectRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithAave: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - ); - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('reject request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithAave: redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; + describe('checkAndRedeemAave()', () => { + it('should not withdraw from Aave when contract has enough balance', async () => { + const { redemptionVaultWithAave, stableCoins, aUSDC } = + await loadFixture(defaultDeploy); + + const usdcAmount = parseUnits('1000', 8); + await stableCoins.usdc.mint( + redemptionVaultWithAave.address, + usdcAmount, + ); + + const balanceBefore = await stableCoins.usdc.balanceOf( + redemptionVaultWithAave.address, + ); + const aTokenBefore = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + + await expect( + redemptionVaultWithAave.checkAndRedeemAave( + stableCoins.usdc.address, + parseUnits('500', 8), + ), + ).not.reverted; + + const aTokenAfter = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + expect(aTokenAfter).to.equal(aTokenBefore); + + const usdcAfter = await stableCoins.usdc.balanceOf( + redemptionVaultWithAave.address, + ); + expect(usdcAfter).to.equal(balanceBefore); + }); + + it('should withdraw missing amount from Aave', async () => { + const { redemptionVaultWithAave, stableCoins, aUSDC } = + await loadFixture(defaultDeploy); + + // Vault has 500 USDC, needs 1000 + const initialUsdc = parseUnits('500', 8); + await stableCoins.usdc.mint( + redemptionVaultWithAave.address, + initialUsdc, + ); + + // Vault has 600 aUSDC + const aTokenAmount = parseUnits('600', 8); + await aUSDC.mint(redemptionVaultWithAave.address, aTokenAmount); + + await redemptionVaultWithAave.checkAndRedeemAave( + stableCoins.usdc.address, + parseUnits('1000', 8), + ); + + // Vault should now have 1000 USDC (500 original + 500 withdrawn from Aave) + const usdcAfter = await stableCoins.usdc.balanceOf( + redemptionVaultWithAave.address, + ); + expect(usdcAfter).to.equal(parseUnits('1000', 8)); + + // aToken balance should decrease by 500 + const aTokenAfter = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + expect(aTokenAfter).to.equal(parseUnits('100', 8)); + }); + + it('should withdraw missing amount from Aave when preferLoanLiquidity=true', async () => { + const { redemptionVaultWithAave, stableCoins, aUSDC, owner } = + await loadFixture(defaultDeploy); + + // Vault has 500 USDC, needs 1000 + const initialUsdc = parseUnits('500', 8); + await stableCoins.usdc.mint( + redemptionVaultWithAave.address, + initialUsdc, + ); + + // Vault has 600 aUSDC + const aTokenAmount = parseUnits('600', 8); + await aUSDC.mint(redemptionVaultWithAave.address, aTokenAmount); + + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithAave, owner }, + true, + ); + + await redemptionVaultWithAave.checkAndRedeemAave( + stableCoins.usdc.address, + parseUnits('1000', 8), + ); + + // Vault should now have 1000 USDC (500 original + 500 withdrawn from Aave) + const usdcAfter = await stableCoins.usdc.balanceOf( + redemptionVaultWithAave.address, + ); + expect(usdcAfter).to.equal(parseUnits('1000', 8)); + + // aToken balance should decrease by 500 + const aTokenAfter = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + expect(aTokenAfter).to.equal(parseUnits('100', 8)); + }); + + it('should revert: when Aave withdraws less than missing amount', async () => { + const { redemptionVaultWithAave, stableCoins, aUSDC, aavePoolMock } = + await loadFixture(defaultDeploy); + + // Vault has 200 USDC, needs 1000 + await stableCoins.usdc.mint( + redemptionVaultWithAave.address, + parseUnits('200', 8), + ); + + // Vault has enough aTokens to cover the gap + await aUSDC.mint( + redemptionVaultWithAave.address, + parseUnits('1000', 8), + ); + + // Simulate partial Aave withdrawal + await aavePoolMock.setWithdrawReturnBps(5000); + + await expect( + redemptionVaultWithAave.checkAndRedeemAave( + stableCoins.usdc.address, + parseUnits('1000', 8), + ), + ).to.be.revertedWithCustomError( + redemptionVaultWithAave, + 'InsufficientWithdrawnAmount', + ); + }); + }); - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - ); + describe('redeemInstant()', () => { + describe('preferLoanLiquidity=true', () => { + it('redeem 100 mTBILL when vault has enough USDC (no Aave needed)', async () => { + const { + owner, + redemptionVaultWithAave, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + aUSDC, + } = await loadFixture(defaultDeploy); + + await mintToken(stableCoins.usdc, redemptionVaultWithAave, 100000); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + const aTokenBefore = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithAave, owner }, + true, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.usdc, + 100, + ); + + // aToken balance should not change + const aTokenAfter = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + expect(aTokenAfter).to.equal(aTokenBefore); + }); + it('redeem 1000 mTBILL when vault has no USDC but has aTokens', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithAave, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + aUSDC, + } = await loadFixture(defaultDeploy); + + // Mint aTokens to vault (enough for redemption) + await aUSDC.mint( + redemptionVaultWithAave.address, + parseUnits('9900', 8), + ); + + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest( + { vault: redemptionVaultWithAave, owner }, + 0, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + const aTokenBefore = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithAave, owner }, + true, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return await aUSDC.balanceOf(redemptionVaultWithAave.address); + }, + }, + stableCoins.usdc, + 1000, + ); + + const aTokenAfter = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + // aTokens should decrease + expect(aTokenAfter).to.be.lt(aTokenBefore); + }); + it('when vault has no USDC but has aTokens and LP liquidity, so LP liquidity should be used', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithAave, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + aUSDC, + loanLp, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadFixture(defaultDeploy); + + // Mint aTokens to vault (enough for redemption) + await aUSDC.mint( + redemptionVaultWithAave.address, + parseUnits('9900', 8), + ); + + await mintToken( + stableCoins.usdc, + redemptionVaultLoanSwapper, + 100000, + ); + await mintToken(mTokenLoan, loanLp, 100000); + await approveBase18( + loanLp, + mTokenLoan, + redemptionVaultWithAave, + 100000, + ); + + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + await setInstantFeeTest( + { vault: redemptionVaultWithAave, owner }, + 0, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + const aTokenBefore = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithAave, owner }, + true, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return await aUSDC.balanceOf(redemptionVaultWithAave.address); + }, + }, + stableCoins.usdc, + 1000, + ); + + const aTokenAfter = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + // aTokens should decrease + expect(aTokenAfter).to.be.eq(aTokenBefore); + }); + + it('when vault partially has USDC, partially has aTokens and partially has LP liquidity, so all the liquidity should be used', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithAave, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + aUSDC, + loanLp, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadFixture(defaultDeploy); + + // Mint aTokens to vault + await aUSDC.mint( + redemptionVaultWithAave.address, + parseUnits('300', 8), + ); + + await mintToken(stableCoins.usdc, redemptionVaultLoanSwapper, 300); + await mintToken(stableCoins.usdc, redemptionVaultWithAave, 400); + await mintToken(mTokenLoan, loanLp, 300); + await approveBase18( + loanLp, + mTokenLoan, + redemptionVaultWithAave, + 300, + ); + + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + await setInstantFeeTest( + { vault: redemptionVaultWithAave, owner }, + 0, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithAave, owner }, + true, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return await aUSDC.balanceOf(redemptionVaultWithAave.address); + }, + }, + stableCoins.usdc, + 1000, + ); + + const aTokenAfter = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + + // liquidity should be used up + expect(aTokenAfter).to.be.eq(0); + expect( + await stableCoins.usdc.balanceOf(redemptionVaultWithAave.address), + ).to.be.eq(0); + expect( + await stableCoins.usdc.balanceOf( + redemptionVaultLoanSwapper.address, + ), + ).to.be.eq(0); + expect(await mTokenLoan.balanceOf(loanLp.address)).to.be.eq(0); + }); + }); + + it('redeem 100 mTBILL when vault has enough USDC (no Aave needed)', async () => { + const { + owner, + redemptionVaultWithAave, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + aUSDC, + } = await loadFixture(defaultDeploy); + + await mintToken(stableCoins.usdc, redemptionVaultWithAave, 100000); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + const aTokenBefore = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.usdc, + 100, + ); + + // aToken balance should not change + const aTokenAfter = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + expect(aTokenAfter).to.equal(aTokenBefore); + }); + + it('redeem 1000 mTBILL when vault has no USDC but has aTokens', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithAave, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + aUSDC, + } = await loadFixture(defaultDeploy); + + // Mint aTokens to vault (enough for redemption) + await aUSDC.mint( + redemptionVaultWithAave.address, + parseUnits('9900', 8), + ); + + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest({ vault: redemptionVaultWithAave, owner }, 0); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + const aTokenBefore = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return await aUSDC.balanceOf(redemptionVaultWithAave.address); + }, + }, + stableCoins.usdc, + 1000, + ); + + const aTokenAfter = await aUSDC.balanceOf( + redemptionVaultWithAave.address, + ); + // aTokens should decrease + expect(aTokenAfter).to.be.lt(aTokenBefore); + }); + + it('redeem 1000 mTBILL when vault has 100 USDC and sufficient aTokens (partial Aave)', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithAave, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + aUSDC, + } = await loadFixture(defaultDeploy); + + // Vault has 100 USDC + 9900 aTokens + await mintToken(stableCoins.usdc, redemptionVaultWithAave, 100); + await aUSDC.mint( + redemptionVaultWithAave.address, + parseUnits('9900', 8), + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return await aUSDC.balanceOf(redemptionVaultWithAave.address); + }, + }, + stableCoins.usdc, + 1000, + ); + }); + + it('redeem 1000 mTBILL when vault has 100 USDC and insufficient aTokens (partial Aave, partial lp flow)', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithAave, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + aUSDC, + loanLp, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadFixture(defaultDeploy); + + // Vault has 100 USDC + 9900 aTokens + await mintToken(stableCoins.usdc, redemptionVaultWithAave, 100); + await mintToken(mTokenLoan, loanLp, 200); + await approveBase18(loanLp, mTokenLoan, redemptionVaultWithAave, 200); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await mintToken(stableCoins.usdc, redemptionVaultLoanSwapper, 200); + + await aUSDC.mint( + redemptionVaultWithAave.address, + parseUnits('700', 8), + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + const loanRequestId = + await redemptionVaultWithAave.currentLoanRequestId(); + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return await aUSDC.balanceOf(redemptionVaultWithAave.address); + }, + }, + stableCoins.usdc, + 1000, + ); + const loanRequest = await redemptionVaultWithAave.loanRequests( + loanRequestId, + ); + expect(loanRequest.status).eq(0); + expect(loanRequest.amountTokenOut).eq(parseUnits('198')); + }); + + it('redeem 1000 mTBILL with different prices (stable 1.03$, mToken 5$) and partial Aave', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithAave, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + aUSDC, + } = await loadFixture(defaultDeploy); + + await mintToken(stableCoins.usdc, redemptionVaultWithAave, 100); + await aUSDC.mint( + redemptionVaultWithAave.address, + parseUnits('15000', 8), + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await redemptionVaultWithAave.freeFromMinAmount(owner.address, true); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return await aUSDC.balanceOf(redemptionVaultWithAave.address); + }, + }, + stableCoins.usdc, + 1000, + ); + }); + + it('redeem 1000 mTBILL with waived fee and Aave withdrawal', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithAave, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + aUSDC, + } = await loadFixture(defaultDeploy); + + await mintToken(stableCoins.usdc, redemptionVaultWithAave, 100); + await aUSDC.mint( + redemptionVaultWithAave.address, + parseUnits('15000', 8), + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await setWaivedFeeAccountTest( + { vault: redemptionVaultWithAave, owner }, + owner.address, + true, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithAave, + owner, + mTBILL, + mTokenToUsdDataFeed, + waivedFee: true, + additionalLiquidity: async () => { + return await aUSDC.balanceOf(redemptionVaultWithAave.address); + }, + }, + stableCoins.usdc, + 1000, + ); + }); + + it('should fail: insufficient aToken balance during redeemInstant and it hits loan lp flow', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithAave, + stableCoins, + mTBILL, + dataFeed, + aUSDC, + } = await loadFixture(defaultDeploy); + + await setLoanLpTest( + { redemptionVault: redemptionVaultWithAave, owner }, + ethers.constants.AddressZero, + ); + + // Vault has no USDC and only 10 aTokens (not enough for 1000 mTBILL redemption) + await aUSDC.mint( + redemptionVaultWithAave.address, + parseUnits('10', 8), + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest({ vault: redemptionVaultWithAave, owner }, 0); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + await expect( + redemptionVaultWithAave['redeemInstant(address,uint256,uint256)']( + stableCoins.usdc.address, + parseUnits('1000'), + 0, + ), + ).to.be.revertedWith('ERC20: transfer amount exceeds balance'); + }); + + it('should fail: when aave pool is not configured and it hits loan lp flow', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithAave, + stableCoins, + mTBILL, + dataFeed, + } = await loadFixture(defaultDeploy); + + await setLoanLpTest( + { redemptionVault: redemptionVaultWithAave, owner }, + ethers.constants.AddressZero, + ); + // Vault has no USDC and only 10 aTokens (not enough for 1000 mTBILL redemption) + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest({ vault: redemptionVaultWithAave, owner }, 0); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + await redemptionVaultWithAave.removeAavePool( + stableCoins.usdc.address, + ); + + await expect( + redemptionVaultWithAave['redeemInstant(address,uint256,uint256)']( + stableCoins.usdc.address, + parseUnits('1000'), + 0, + ), + ).to.be.revertedWith('ERC20: transfer amount exceeds balance'); + }); + + it('should fail: when aave pool is configured but aToken is not in the pool and it hits loan lp flow', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithAave, + stableCoins, + mTBILL, + dataFeed, + aavePoolMock, + } = await loadFixture(defaultDeploy); + + await setLoanLpTest( + { redemptionVault: redemptionVaultWithAave, owner }, + ethers.constants.AddressZero, + ); + + // Vault has no USDC and only 10 aTokens (not enough for 1000 mTBILL redemption) + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest({ vault: redemptionVaultWithAave, owner }, 0); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + await aavePoolMock.setReserveAToken( + stableCoins.usdc.address, + ethers.constants.AddressZero, + ); + + await expect( + redemptionVaultWithAave['redeemInstant(address,uint256,uint256)']( + stableCoins.usdc.address, + parseUnits('1000'), + 0, + ), + ).to.be.revertedWith('ERC20: transfer amount exceeds balance'); + }); + + it('should fail: Aave pool has insufficient liquidity during redeemInstant', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithAave, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed: _mTokenToUsdDataFeed, + aUSDC, + aavePoolMock, + } = await loadFixture(defaultDeploy); + + // Vault has aTokens but pool has no liquidity + await aUSDC.mint( + redemptionVaultWithAave.address, + parseUnits('10000', 8), + ); + await mintToken(mTBILL, owner, 100000); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100000); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest({ vault: redemptionVaultWithAave, owner }, 0); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + // Drain the pool + const poolBalance = await stableCoins.usdc.balanceOf( + aavePoolMock.address, + ); + await aavePoolMock.withdrawAdmin( + stableCoins.usdc.address, + ( + await ethers.getSigners() + )[10].address, + poolBalance, + ); + + await expect( + redemptionVaultWithAave['redeemInstant(address,uint256,uint256)']( + stableCoins.usdc.address, + parseUnits('1000'), + 0, + ), + ).to.be.revertedWith('ERC20: transfer amount exceeds balance'); + }); + + it('should fail: short Aave withdrawal during redeemInstant', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithAave, + stableCoins, + mTBILL, + dataFeed, + aUSDC, + aavePoolMock, + } = await loadFixture(defaultDeploy); + + await aUSDC.mint( + redemptionVaultWithAave.address, + parseUnits('10000', 8), + ); + await mintToken(mTBILL, owner, 100000); + await approveBase18(owner, mTBILL, redemptionVaultWithAave, 100000); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest({ vault: redemptionVaultWithAave, owner }, 0); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await aavePoolMock.setWithdrawReturnBps(5000); + + await expect( + redemptionVaultWithAave['redeemInstant(address,uint256,uint256)']( + stableCoins.usdc.address, + parseUnits('1000'), + 0, + ), + ).to.be.revertedWith('ERC20: transfer amount exceeds balance'); + }); + }); }); - }); -}); + }, +); diff --git a/test/unit/RedemptionVaultWithBUIDL.test.ts b/test/unit/RedemptionVaultWithBUIDL.test.ts deleted file mode 100644 index 36f92de5..00000000 --- a/test/unit/RedemptionVaultWithBUIDL.test.ts +++ /dev/null @@ -1,4724 +0,0 @@ -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; -import { expect } from 'chai'; -import { constants } from 'ethers'; -import { parseUnits } from 'ethers/lib/utils'; -import { ethers } from 'hardhat'; - -import { encodeFnSelector } from '../../helpers/utils'; -import { - EUsdRedemptionVaultWithBUIDL__factory, - ManageableVaultTester__factory, - MBasisRedemptionVaultWithBUIDL__factory, - RedemptionVaultWithBUIDLTest__factory, -} from '../../typechain-types'; -import { acErrors, blackList, greenList } from '../common/ac.helpers'; -import { - approveBase18, - mintToken, - pauseVault, - pauseVaultFn, -} from '../common/common.helpers'; -import { setRoundData } from '../common/data-feed.helpers'; -import { defaultDeploy } from '../common/fixtures'; -import { - addPaymentTokenTest, - addWaivedFeeAccountTest, - changeTokenAllowanceTest, - removePaymentTokenTest, - removeWaivedFeeAccountTest, - setInstantFeeTest, - setInstantDailyLimitTest, - setMinAmountTest, - setVariabilityToleranceTest, - withdrawTest, - changeTokenFeeTest, - setMinBuidlToRedeem, -} from '../common/manageable-vault.helpers'; -import { - approveRedeemRequestTest, - redeemFiatRequestTest, - redeemInstantTest, - redeemRequestTest, - rejectRedeemRequestTest, - safeApproveRedeemRequestTest, - setFiatAdditionalFeeTest, - setMinFiatRedeemAmountTest, -} from '../common/redemption-vault.helpers'; -import { sanctionUser } from '../common/with-sanctions-list.helpers'; - -describe('RedemptionVaultWithBUIDL', function () { - it('deployment', async () => { - const { - redemptionVaultWithBUIDL, - buidlRedemption, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - roles, - } = await loadFixture(defaultDeploy); - - expect(await redemptionVaultWithBUIDL.mToken()).eq(mTBILL.address); - - expect(await redemptionVaultWithBUIDL.ONE_HUNDRED_PERCENT()).eq('10000'); - - expect(await redemptionVaultWithBUIDL.paused()).eq(false); - - expect(await redemptionVaultWithBUIDL.tokensReceiver()).eq( - tokensReceiver.address, - ); - expect(await redemptionVaultWithBUIDL.feeReceiver()).eq( - feeReceiver.address, - ); - - expect(await redemptionVaultWithBUIDL.minAmount()).eq(1000); - expect(await redemptionVaultWithBUIDL.minFiatRedeemAmount()).eq(1000); - - expect(await redemptionVaultWithBUIDL.instantFee()).eq('100'); - - expect(await redemptionVaultWithBUIDL.instantDailyLimit()).eq( - parseUnits('100000'), - ); - - expect(await redemptionVaultWithBUIDL.mTokenDataFeed()).eq( - mTokenToUsdDataFeed.address, - ); - expect(await redemptionVaultWithBUIDL.variationTolerance()).eq(1); - - expect(await redemptionVaultWithBUIDL.vaultRole()).eq( - roles.tokenRoles.mTBILL.redemptionVaultAdmin, - ); - - expect(await redemptionVaultWithBUIDL.MANUAL_FULLFILMENT_TOKEN()).eq( - ethers.constants.AddressZero, - ); - - expect(await redemptionVaultWithBUIDL.buidlRedemption()).eq( - buidlRedemption.address, - ); - expect(await redemptionVaultWithBUIDL.minBuidlToRedeem()).eq( - parseUnits('250000', 6), - ); - }); - - it('failing deployment', async () => { - const { - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - accessControl, - mockedSanctionsList, - owner, - } = await loadFixture(defaultDeploy); - - const redemptionVaultWithBUIDL = - await new RedemptionVaultWithBUIDLTest__factory(owner).deploy(); - - await expect( - redemptionVaultWithBUIDL[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address)' - ]( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - { - fiatAdditionalFee: 10000, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: parseUnits('100'), - }, - constants.AddressZero, - ), - ).to.be.reverted; - }); - - it('MBasisRedemptionVault', async () => { - const fixture = await loadFixture(defaultDeploy); - - const tester = await new MBasisRedemptionVaultWithBUIDL__factory( - fixture.owner, - ).deploy(); - - expect(await tester.vaultRole()).eq( - await tester.M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE(), - ); - }); - - it('EUsdRedemptionVault', async () => { - const fixture = await loadFixture(defaultDeploy); - - const tester = await new EUsdRedemptionVaultWithBUIDL__factory( - fixture.owner, - ).deploy(); - - expect(await tester.vaultRole()).eq( - await tester.E_USD_REDEMPTION_VAULT_ADMIN_ROLE(), - ); - - expect(await tester.greenlistedRole()).eq( - await tester.E_USD_GREENLISTED_ROLE(), - ); - }); - - describe('initialization', () => { - it('should fail: cal; initialize() when already initialized', async () => { - const { redemptionVaultWithBUIDL } = await loadFixture(defaultDeploy); - - await expect( - redemptionVaultWithBUIDL[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address,uint256,uint256)' - ]( - constants.AddressZero, - { - mToken: constants.AddressZero, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: constants.AddressZero, - tokensReceiver: constants.AddressZero, - }, - { - instantFee: 0, - instantDailyLimit: 0, - }, - constants.AddressZero, - 0, - 0, - { - fiatAdditionalFee: 0, - fiatFlatFee: 0, - minFiatRedeemAmount: 0, - }, - constants.AddressZero, - constants.AddressZero, - 0, - 0, - ), - ).revertedWith('Initializable: contract is already initialized'); - }); - - it('should fail: call with initializing == false', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initializeWithoutInitializer( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('Initializable: contract is not initializing'); - }); - - it('should fail: when _tokensReceiver == address(this)', async () => { - const { - owner, - accessControl, - mTBILL, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: vault.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('invalid address'); - }); - it('should fail: when _feeReceiver == address(this)', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: vault.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('invalid address'); - }); - it('should fail: when limit = 0', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: 0, - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('zero limit'); - }); - it('should fail: when mToken dataFeed address zero', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('zero address'); - }); - it('should fail: when variationTolarance zero', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mockedSanctionsList, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 0, - 1000, - ), - ).revertedWith('fee == 0'); - }); - }); - - describe('setMinBuidlToRedeem()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithBUIDL, regularAccounts } = - await loadFixture(defaultDeploy); - - await setMinBuidlToRedeem( - { vault: redemptionVaultWithBUIDL, owner }, - parseUnits('100000', 6), - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithBUIDL } = await loadFixture( - defaultDeploy, - ); - await setMinBuidlToRedeem( - { vault: redemptionVaultWithBUIDL, owner }, - parseUnits('100000', 6), - ); - }); - }); - - describe('setMinAmount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithBUIDL, regularAccounts } = - await loadFixture(defaultDeploy); - - await setMinAmountTest({ vault: redemptionVaultWithBUIDL, owner }, 1.1, { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithBUIDL } = await loadFixture( - defaultDeploy, - ); - await setMinAmountTest({ vault: redemptionVaultWithBUIDL, owner }, 1.1); - }); - }); - - describe('setMinFiatRedeemAmount()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithBUIDL, regularAccounts } = - await loadFixture(defaultDeploy); - - await setMinFiatRedeemAmountTest( - { redemptionVault: redemptionVaultWithBUIDL, owner }, - 1.1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithBUIDL } = await loadFixture( - defaultDeploy, - ); - await setMinFiatRedeemAmountTest( - { redemptionVault: redemptionVaultWithBUIDL, owner }, - 1.1, - ); - }); - }); - - describe('setFiatAdditionalFee()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithBUIDL, regularAccounts } = - await loadFixture(defaultDeploy); - - await setFiatAdditionalFeeTest( - { redemptionVault: redemptionVaultWithBUIDL, owner }, - 100, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithBUIDL } = await loadFixture( - defaultDeploy, - ); - await setFiatAdditionalFeeTest( - { redemptionVault: redemptionVaultWithBUIDL, owner }, - 100, - ); - }); - }); - - describe('setInstantDailyLimit()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithBUIDL, regularAccounts } = - await loadFixture(defaultDeploy); - - await setInstantDailyLimitTest( - { vault: redemptionVaultWithBUIDL, owner }, - parseUnits('1000'), - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('should fail: try to set 0 limit', async () => { - const { owner, redemptionVaultWithBUIDL } = await loadFixture( - defaultDeploy, - ); - - await setInstantDailyLimitTest( - { vault: redemptionVaultWithBUIDL, owner }, - constants.Zero, - { - revertMessage: 'MV: limit zero', - }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithBUIDL } = await loadFixture( - defaultDeploy, - ); - await setInstantDailyLimitTest( - { vault: redemptionVaultWithBUIDL, owner }, - parseUnits('1000'), - ); - }); - }); - - describe('addPaymentToken()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - ethers.constants.AddressZero, - ethers.constants.AddressZero, - 0, - true, - constants.MaxUint256, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when token is already added', async () => { - const { redemptionVaultWithBUIDL, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - constants.MaxUint256, - { - revertMessage: 'MV: already added', - }, - ); - }); - - it('should fail: when token dataFeed address zero', async () => { - const { redemptionVaultWithBUIDL, stableCoins, owner } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - constants.AddressZero, - 0, - true, - constants.MaxUint256, - { - revertMessage: 'zero address', - }, - ); - }); - - it('call when allowance is zero', async () => { - const { redemptionVaultWithBUIDL, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - constants.Zero, - ); - }); - - it('call when allowance is not uint256 max', async () => { - const { redemptionVaultWithBUIDL, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - parseUnits('100'), - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role and add 3 options on a row', async () => { - const { redemptionVaultWithBUIDL, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - }); - }); - - describe('addWaivedFeeAccount()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithBUIDL, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if account fee already waived', async () => { - const { redemptionVaultWithBUIDL, owner } = await loadFixture( - defaultDeploy, - ); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithBUIDL, owner }, - owner.address, - ); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithBUIDL, owner }, - owner.address, - { revertMessage: 'MV: already added' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, owner } = await loadFixture( - defaultDeploy, - ); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithBUIDL, owner }, - owner.address, - ); - }); - }); - - describe('removeWaivedFeeAccount()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await removeWaivedFeeAccountTest( - { vault: redemptionVaultWithBUIDL, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if account not found in restriction', async () => { - const { redemptionVaultWithBUIDL, owner } = await loadFixture( - defaultDeploy, - ); - await removeWaivedFeeAccountTest( - { vault: redemptionVaultWithBUIDL, owner }, - owner.address, - { revertMessage: 'MV: not found' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, owner } = await loadFixture( - defaultDeploy, - ); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithBUIDL, owner }, - owner.address, - ); - await removeWaivedFeeAccountTest( - { vault: redemptionVaultWithBUIDL, owner }, - owner.address, - ); - }); - }); - - describe('setFee()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setInstantFeeTest( - { vault: redemptionVaultWithBUIDL, owner }, - ethers.constants.Zero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: if new value greater then 100%', async () => { - const { redemptionVaultWithBUIDL, owner } = await loadFixture( - defaultDeploy, - ); - await setInstantFeeTest( - { vault: redemptionVaultWithBUIDL, owner }, - 10001, - { - revertMessage: 'fee > 100%', - }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, owner } = await loadFixture( - defaultDeploy, - ); - await setInstantFeeTest({ vault: redemptionVaultWithBUIDL, owner }, 100); - }); - }); - - describe('setVariabilityTolerance()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setVariabilityToleranceTest( - { vault: redemptionVaultWithBUIDL, owner }, - ethers.constants.Zero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if new value zero', async () => { - const { redemptionVaultWithBUIDL, owner } = await loadFixture( - defaultDeploy, - ); - await setVariabilityToleranceTest( - { vault: redemptionVaultWithBUIDL, owner }, - ethers.constants.Zero, - { revertMessage: 'fee == 0' }, - ); - }); - - it('should fail: if new value greater then 100%', async () => { - const { redemptionVaultWithBUIDL, owner } = await loadFixture( - defaultDeploy, - ); - await setVariabilityToleranceTest( - { vault: redemptionVaultWithBUIDL, owner }, - 10001, - { revertMessage: 'fee > 100%' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, owner } = await loadFixture( - defaultDeploy, - ); - await setVariabilityToleranceTest( - { vault: redemptionVaultWithBUIDL, owner }, - 100, - ); - }); - }); - - describe('removePaymentToken()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await removePaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when token is not exists', async () => { - const { owner, redemptionVaultWithBUIDL, stableCoins } = - await loadFixture(defaultDeploy); - await removePaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai.address, - { revertMessage: 'MV: not exists' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await removePaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai.address, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role and add 3 options on a row', async () => { - const { redemptionVaultWithBUIDL, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - - await removePaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai.address, - ); - await removePaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc.address, - ); - await removePaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdt.address, - ); - - await removePaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdt.address, - { revertMessage: 'MV: not exists' }, - ); - }); - }); - - describe('withdrawToken()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await withdrawTest( - { vault: redemptionVaultWithBUIDL, owner }, - ethers.constants.AddressZero, - 0, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when there is no token in vault', async () => { - const { owner, redemptionVaultWithBUIDL, regularAccounts, stableCoins } = - await loadFixture(defaultDeploy); - await withdrawTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - 1, - regularAccounts[0], - { revertMessage: 'ERC20: transfer amount exceeds balance' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, regularAccounts, stableCoins, owner } = - await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, redemptionVaultWithBUIDL, 1); - await withdrawTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - 1, - regularAccounts[0], - ); - }); - }); - - describe('freeFromMinAmount()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithBUIDL, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithBUIDL - .connect(regularAccounts[0]) - .freeFromMinAmount(regularAccounts[1].address, true), - ).to.be.revertedWith('WMAC: hasnt role'); - }); - it('should not fail', async () => { - const { redemptionVaultWithBUIDL, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithBUIDL.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.not.reverted; - - expect( - await redemptionVaultWithBUIDL.isFreeFromMinAmount( - regularAccounts[0].address, - ), - ).to.eq(true); - }); - it('should fail: already in list', async () => { - const { redemptionVaultWithBUIDL, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithBUIDL.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.not.reverted; - - expect( - await redemptionVaultWithBUIDL.isFreeFromMinAmount( - regularAccounts[0].address, - ), - ).to.eq(true); - - await expect( - redemptionVaultWithBUIDL.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.revertedWith('DV: already free'); - }); - }); - - describe('changeTokenAllowance()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithBUIDL, owner }, - ethers.constants.AddressZero, - 0, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: token not exist', async () => { - const { redemptionVaultWithBUIDL, owner, stableCoins } = - await loadFixture(defaultDeploy); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: token not exists' }, - ); - }); - it('should fail: allowance zero', async () => { - const { redemptionVaultWithBUIDL, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: zero allowance' }, - ); - }); - it('should fail: if mint exceed allowance', async () => { - const { - redemptionVaultWithBUIDL, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, owner, 100000); - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100000); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100000); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc.address, - 100, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai.address, - 100000000, - ); - }); - it('should decrease if allowance < UINT_MAX', async () => { - const { - redemptionVaultWithBUIDL, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, owner, 100000); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100000); - - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc.address, - parseUnits('1000'), - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - const tokenConfigBefore = await redemptionVaultWithBUIDL.tokensConfig( - stableCoins.usdc.address, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 999, - ); - - const tokenConfigAfter = await redemptionVaultWithBUIDL.tokensConfig( - stableCoins.usdc.address, - ); - - expect(tokenConfigBefore.allowance.sub(tokenConfigAfter.allowance)).eq( - parseUnits('999'), - ); - }); - it('should not decrease if allowance = UINT_MAX', async () => { - const { - redemptionVaultWithBUIDL, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, owner, 100000); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100000); - - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc.address, - constants.MaxUint256, - ); - - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - const tokenConfigBefore = await redemptionVaultWithBUIDL.tokensConfig( - stableCoins.usdc.address, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 999, - ); - - const tokenConfigAfter = await redemptionVaultWithBUIDL.tokensConfig( - stableCoins.usdc.address, - ); - - expect(tokenConfigBefore.allowance).eq(tokenConfigAfter.allowance); - }); - }); - - describe('changeTokenFee()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await changeTokenFeeTest( - { vault: redemptionVaultWithBUIDL, owner }, - ethers.constants.AddressZero, - 0, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: token not exist', async () => { - const { redemptionVaultWithBUIDL, owner, stableCoins } = - await loadFixture(defaultDeploy); - await changeTokenFeeTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: token not exists' }, - ); - }); - it('should fail: fee > 100%', async () => { - const { redemptionVaultWithBUIDL, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai.address, - 10001, - { revertMessage: 'fee > 100%' }, - ); - }); - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithBUIDL, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai.address, - 100, - ); - }); - }); - - describe('redeemInstant()', () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 0, - { - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithBUIDL, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemInstant(address,uint256,uint256)', - ); - await pauseVaultFn(redemptionVaultWithBUIDL, selector); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'ERC20: burn amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mockedAggregatorMToken, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18( - owner, - stableCoins.usdc, - redemptionVaultWithBUIDL, - 10, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await mintToken(mTBILL, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 0); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: if min receive amount greater then actual', async () => { - const { - redemptionVaultWithBUIDL, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(mTBILL, owner, 100_000); - - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100_000); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - minAmount: parseUnits('1000000'), - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'RVB: minReceiveAmount > actual', - }, - ); - }); - - it('should fail: call for amount < minAmount', async () => { - const { - redemptionVaultWithBUIDL, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100_000); - - await setMinAmountTest( - { vault: redemptionVaultWithBUIDL, owner }, - 100_000, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'RV: amount < min', - }, - ); - }); - - it('should fail: call when token is invalid', async () => { - const { - redemptionVaultWithBUIDL, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'RVB: invalid token', - }, - ); - }); - - it('should fail: if exceed allowance of redeem by token', async () => { - const { - redemptionVaultWithBUIDL, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(mTBILL, owner, 100_000); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc.address, - 100, - ); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100_000); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - - it('should fail: if redeem daily limit exceeded', async () => { - const { - redemptionVaultWithBUIDL, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(mTBILL, owner, 100_000); - await setInstantDailyLimitTest( - { vault: redemptionVaultWithBUIDL, owner }, - 1000, - ); - - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100_000); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'MV: exceed limit', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 10000, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'RV: amountMTokenIn < fee', - }, - ); - - await removePaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest( - { vault: redemptionVaultWithBUIDL, owner }, - 10000, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { revertMessage: 'RV: amountMTokenIn < fee' }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist ', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithBUIDL.setGreenlistEnable(true); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist ', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: when function with custom recipient is paused', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithBUIDL, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemInstant(address,uint256,uint256,address)', - ); - await pauseVaultFn(redemptionVaultWithBUIDL, selector); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithBUIDL.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: user try to instant redeem more then contract can redeem', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - buidl, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100000); - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100); - await mintToken(buidl, redemptionVaultWithBUIDL, 100); - - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100000); - - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 100, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100000, - { - revertMessage: 'RVB: buidlToRedeem > balance', - }, - ); - }); - - it('redeem 100 mTBILL, when price of stable is 1$ and mToken price is 1$ and contract have 100 USDC', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - }); - - it('redeem 1000 mTBILL, when price of stable is 1$ and mToken price is 1$ and contract do not have USDC, but have 9900 BUIDL', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - buidl, - } = await loadFixture(defaultDeploy); - - await mintToken(buidl, redemptionVaultWithBUIDL, 9900); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: redemptionVaultWithBUIDL, owner }, 0); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - const buidlBalanceBefore = await buidl.balanceOf( - redemptionVaultWithBUIDL.address, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1000, - ); - const buidlBalanceAfter = await buidl.balanceOf( - redemptionVaultWithBUIDL.address, - ); - expect(buidlBalanceAfter).eq( - buidlBalanceBefore.sub(parseUnits('250000', 6)), - ); - }); - - it('redeem 1000 mTBILL, when price of stable is 1$ and mToken price is 1$ and contract have 100 USDC and 9900 BUIDL', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - buidl, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100); - await mintToken(buidl, redemptionVaultWithBUIDL, 9900); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1000, - ); - }); - - it('redeem 1000 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and contract have 100 USDC and 15000 BUIDL without checking of minDepositAmount', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - buidl, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100); - await mintToken(buidl, redemptionVaultWithBUIDL, 15000); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redemptionVaultWithBUIDL.freeFromMinAmount(owner.address, true); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1000, - ); - }); - - it('redeem 1000 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and contract have 100 USDC and 15000 BUIDL and user in waivedFeeRestriction', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - buidl, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100); - await mintToken(buidl, redemptionVaultWithBUIDL, 15000); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithBUIDL, owner }, - owner.address, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - waivedFee: true, - }, - stableCoins.usdc, - 1000, - ); - }); - - it('redeem 100 mTBILL (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithBUIDL, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when recipient == msg.sender (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithBUIDL, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[0], - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when other fn overload is paused (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithBUIDL, - encodeFnSelector('redeemInstant(address,uint256,uint256)'), - ); - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithBUIDL, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when other fn overload is paused', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithBUIDL, - encodeFnSelector('redeemInstant(address,uint256,uint256,address)'), - ); - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithBUIDL, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - }); - - describe('redeemRequest()', () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 0, - { - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithBUIDL, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector('redeemRequest(address,uint256)'); - await pauseVaultFn(redemptionVaultWithBUIDL, selector); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mockedAggregatorMToken, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, redemptionVaultWithBUIDL, 10); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(mTBILL, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 0); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minAmount', async () => { - const { - redemptionVaultWithBUIDL, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100_000); - - await setMinAmountTest( - { vault: redemptionVaultWithBUIDL, owner }, - 100_000, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'RV: amount < min', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 10000, - true, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'RV: amountMTokenIn < fee', - }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist ', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithBUIDL.setGreenlistEnable(true); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist ', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: when function paused (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithBUIDL, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemRequest(address,uint256,address)', - ); - await pauseVaultFn(redemptionVaultWithBUIDL, selector); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithBUIDL.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: user try to redeem fiat in basic request (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100); - - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - await redemptionVaultWithBUIDL.MANUAL_FULLFILMENT_TOKEN(), - 100, - { - revertMessage: 'RV: tokenOut == fiat', - }, - ); - }); - - it('should fail: user try to redeem fiat in basic request', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100); - - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - await redemptionVaultWithBUIDL.MANUAL_FULLFILMENT_TOKEN(), - 100, - { - revertMessage: 'RV: tokenOut == fiat', - }, - ); - }); - - it('redeem request 100 mTBILL, greenlist enabled and user in greenlist ', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithBUIDL.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(stableCoins.dai, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithBUIDL, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redemptionVaultWithBUIDL.freeFromMinAmount(owner.address, true); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithBUIDL, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithBUIDL, owner }, - owner.address, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - waivedFee: true, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem request 100 mTBILL (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithBUIDL, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem request 100 mTBILL when recipient == msg.sender (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithBUIDL, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[0], - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem request 100 mTBILL when other fn overload is paused (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithBUIDL, - encodeFnSelector('redeemRequest(address,uint256)'), - ); - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithBUIDL, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem request 100 mTBILL when other fn overload is paused', async () => { - const { - owner, - redemptionVaultWithBUIDL, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithBUIDL, - encodeFnSelector('redeemRequest(address,uint256,address)'), - ); - await mintToken(stableCoins.usdc, redemptionVaultWithBUIDL, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithBUIDL, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithBUIDL, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithBUIDL, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - }); - - describe('redeemFiatRequest()', () => { - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVaultWithBUIDL: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - { - from: regularAccounts[0], - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - redemptionVaultWithBUIDL: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - redemptionVaultWithBUIDL: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVault, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector('redeemFiatRequest(uint256)'); - await pauseVaultFn(redemptionVault, selector); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - redemptionVaultWithBUIDL: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await approveBase18(owner, mTBILL, redemptionVault, 100); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - redemptionVaultWithBUIDL: redemptionVault, - mTBILL, - mockedAggregatorMToken, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100_000); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 0); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 10, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minFiatRedeemAmount', async () => { - const { - redemptionVaultWithBUIDL: redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100_000); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setMinFiatRedeemAmountTest({ redemptionVault, owner }, 100_000); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 99_999, - { - revertMessage: 'RV: amount < min', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - redemptionVaultWithBUIDL: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await setFiatAdditionalFeeTest({ redemptionVault, owner }, 10000); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - { - revertMessage: 'RV: amountMTokenIn < fee', - }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist ', async () => { - const { - owner, - redemptionVaultWithBUIDL: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVault.setGreenlistEnable(true); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist ', async () => { - const { - owner, - redemptionVaultWithBUIDL: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - regularAccounts, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - redemptionVaultWithBUIDL: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('redeem fiat request 100 mTBILL, greenlist enabled and user in greenlist ', async () => { - const { - owner, - redemptionVaultWithBUIDL: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await redemptionVault.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem fiat request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - ); - }); - - it('redeem fiat request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - ); - }); - - it('redeem fiat request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redemptionVault.freeFromMinAmount(owner.address, true); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - ); - }); - - it('redeem fiat request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await addWaivedFeeAccountTest( - { vault: redemptionVault, owner }, - owner.address, - ); - await redeemFiatRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - waivedFee: true, - }, - 100, - ); - }); - }); - - describe('approveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithBUIDL: redemptionVault, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await approveRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithBUIDL: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - parseUnits('1'), - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - }); - }); - - describe('approveRequest() with fiat', async () => { - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - ); - const requestId = 0; - await changeTokenAllowanceTest( - { vault: redemptionVault, owner }, - constants.AddressZero, - parseUnits('100'), - ); - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - }); - }); - - describe('safeApproveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithBUIDL: redemptionVault, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await safeApproveRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithBUIDL: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - parseUnits('1'), - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: if new rate greater then variabilityTolerance', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('6'), - { revertMessage: 'MV: exceed price diviation' }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.000001'), - ); - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.00001'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('safe approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL: redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.000001'), - ); - }); - }); - - describe('rejectRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithBUIDL: redemptionVault, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await rejectRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithBUIDL: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - ); - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('safe approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithBUIDL: redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - ); - }); - }); - - describe('redeemInstant() complex', () => { - it('should fail: when is paused', async () => { - const { - redemptionVaultWithBUIDL: redemptionVault, - owner, - mTBILL, - stableCoins, - regularAccounts, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(redemptionVault); - await mintToken(stableCoins.usdc, redemptionVault, 100); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - redemptionVault, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('is on pause, but admin can use everything', async () => { - const { - redemptionVaultWithBUIDL: redemptionVault, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(redemptionVault); - - await mintToken(stableCoins.usdc, redemptionVault, 100); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 100, - { - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('call for amount == minAmount, if USDC balance minAmount/2 and BUIDL balance minAmount/2', async () => { - const { - redemptionVaultWithBUIDL: redemptionVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - buidl, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await mintToken(stableCoins.usdc, redemptionVault, 50_000); - await mintToken(buidl, redemptionVault, 50_000); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 100_000, - ); - }); - - it('redeem 100 mtbill, when price is 5$ and contract balance 100 USDC and 100000 BUIDL, 125 mtbill when price is 5.1$, 114 mtbill when price is 5.4$', async () => { - const { - owner, - mockedAggregator, - redemptionVaultWithBUIDL: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregatorMToken, - buidl, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100 + 125 + 114); - - await mintToken(stableCoins.usdc, redemptionVault, 100); - await mintToken(buidl, redemptionVault, 100000); - - await approveBase18(owner, mTBILL, redemptionVault, 100 + 125 + 114); - - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setRoundData({ mockedAggregator }, 1.04); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 100, - ); - - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5.1); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 125, - ); - - await setRoundData({ mockedAggregator }, 1.01); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5.4); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 114, - ); - }); - }); - - describe('redeemRequest() complex', () => { - it('should fail: when is paused', async () => { - const { - redemptionVaultWithBUIDL: redemptionVault, - owner, - mTBILL, - stableCoins, - regularAccounts, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(redemptionVault); - await mintToken(stableCoins.dai, redemptionVault, 100); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVault, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('is on pause, but admin can use everything', async () => { - const { - redemptionVaultWithBUIDL: redemptionVault, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(redemptionVault); - - await mintToken(stableCoins.dai, redemptionVault, 1000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, stableCoins.dai, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('call for amount == minAmount, then approve', async () => { - const { - redemptionVaultWithBUIDL: redemptionVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100_000, - ); - - const requestId = 0; - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - }); - - it('call for amount == minAmount, then safe approve', async () => { - const { - redemptionVaultWithBUIDL: redemptionVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - requestRedeemer, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: redemptionVault, owner }, 10_000); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 10_000, - ); - - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1.000001'), - ); - }); - - it('call for amount == minAmount, then reject', async () => { - const { - redemptionVaultWithBUIDL: redemptionVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVault, 100_000); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100_000, - ); - - const requestId = 0; - - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - ); - }); - }); -}); diff --git a/test/unit/RedemptionVaultWithMToken.test.ts b/test/unit/RedemptionVaultWithMToken.test.ts index 0210ccb1..5c661b49 100644 --- a/test/unit/RedemptionVaultWithMToken.test.ts +++ b/test/unit/RedemptionVaultWithMToken.test.ts @@ -1,19 +1,27 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; import { BigNumber, constants } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; -import { ethers } from 'hardhat'; + +import { + baseInitParamsRv, + redemptionVaultSuits, +} from './suits/redemption-vault.suits'; import { encodeFnSelector } from '../../helpers/utils'; import { - ManageableVaultTester__factory, + RedemptionVaultWithMToken__factory, + RedemptionVaultWithMTokenTest, RedemptionVaultWithMTokenTest__factory, } from '../../typechain-types'; -import { acErrors, blackList, greenList } from '../common/ac.helpers'; +import { acErrors } from '../common/ac.helpers'; import { approveBase18, + InitializeParamCase, mintToken, pauseVaultFn, + validateImplementation, } from '../common/common.helpers'; import { setRoundData } from '../common/data-feed.helpers'; import { defaultDeploy } from '../common/fixtures'; @@ -21,4105 +29,2435 @@ import { addPaymentTokenTest, setInstantFeeTest, setMinAmountTest, - setInstantDailyLimitTest, - addWaivedFeeAccountTest, - removeWaivedFeeAccountTest, - setVariabilityToleranceTest, - removePaymentTokenTest, - withdrawTest, - changeTokenFeeTest, - changeTokenAllowanceTest, + setWaivedFeeAccountTest, } from '../common/manageable-vault.helpers'; -import { redeemInstantWithMTokenTest } from '../common/redemption-vault-mtoken.helpers'; import { - approveRedeemRequestTest, - redeemFiatRequestTest, - redeemRequestTest, - rejectRedeemRequestTest, - safeApproveRedeemRequestTest, - setFiatAdditionalFeeTest, - setMinFiatRedeemAmountTest, + redeemInstantWithMTokenTest, + setRedemptionVaultTest, +} from '../common/redemption-vault-mtoken.helpers'; +import { + setLoanLpTest, + setLoanSwapperVaultTest, + redeemInstantTest, + setPreferLoanLiquidityTest, } from '../common/redemption-vault.helpers'; -import { sanctionUser } from '../common/with-sanctions-list.helpers'; +import { + initializeRvWithMToken, + InitializerParamsRvWithMToken, +} from '../common/vault-initializer.helpers'; + +const baseInitParamsRvWithMToken = ( + fixture: Parameters[0], +): InitializerParamsRvWithMToken => ({ + ...baseInitParamsRv(fixture), + redemptionVault: fixture.redemptionVaultLoanSwapper, +}); -describe('RedemptionVaultWithMToken', function () { - it('deployment', async () => { - const { - redemptionVaultWithMToken, - redemptionVault, - mFONE, - tokensReceiver, - feeReceiver, - mFoneToUsdDataFeed, - roles, - } = await loadFixture(defaultDeploy); +const rvWithMTokenInitializeParamCases: InitializeParamCase[] = + [ + { + title: 'redemptionVault is zero address', + params: { redemptionVault: constants.AddressZero }, + revertCustomError: { + customErrorName: 'InvalidAddress', + args: [constants.AddressZero], + }, + }, + ]; + +redemptionVaultSuits( + 'RedemptionVaultWithMToken', + defaultDeploy, + { + createNew: async (owner: SignerWithAddress) => + new RedemptionVaultWithMTokenTest__factory(owner).deploy(), + key: 'redemptionVaultWithMToken', + }, + async (fixture) => { + const { redemptionVaultWithMToken, redemptionVaultLoanSwapper } = fixture; + expect(await redemptionVaultWithMToken.redemptionVault()).eq( + redemptionVaultLoanSwapper.address, + ); + await validateImplementation(RedemptionVaultWithMToken__factory); + }, + { + deployUninitialized: (fixture) => + new RedemptionVaultWithMTokenTest__factory(fixture.owner).deploy(), + initialize: async (fixture, params, opt) => { + await initializeRvWithMToken( + { ...baseInitParamsRvWithMToken(fixture), ...params }, + opt?.contract as RedemptionVaultWithMTokenTest, + opt, + ); + }, + extraParamCases: rvWithMTokenInitializeParamCases, + }, + async (defaultDeploy) => { + describe('RedemptionVaultWithMToken', () => { + describe('setRedemptionVault()', () => { + it('should fail: call from address without vault admin role', async () => { + const { redemptionVaultWithMToken, regularAccounts } = + await loadFixture(defaultDeploy); + await expect( + redemptionVaultWithMToken + .connect(regularAccounts[0]) + .setRedemptionVault(regularAccounts[1].address), + ).to.be.revertedWithCustomError( + redemptionVaultWithMToken, + acErrors.WMAC_HASNT_PERMISSION().customErrorName, + ); + }); - expect(await redemptionVaultWithMToken.mToken()).eq(mFONE.address); + it('should fail: zero address', async () => { + const { redemptionVaultWithMToken } = await loadFixture( + defaultDeploy, + ); + await expect( + redemptionVaultWithMToken.setRedemptionVault(constants.AddressZero), + ).to.be.revertedWithCustomError( + redemptionVaultWithMToken, + 'InvalidAddress', + ); + }); - expect(await redemptionVaultWithMToken.ONE_HUNDRED_PERCENT()).eq('10000'); + it('should fail: same address', async () => { + const { redemptionVaultWithMToken, redemptionVaultLoanSwapper } = + await loadFixture(defaultDeploy); + await expect( + redemptionVaultWithMToken.setRedemptionVault( + redemptionVaultLoanSwapper.address, + ), + ).to.be.revertedWithCustomError( + redemptionVaultWithMToken, + 'SameAddressValue', + ); + }); - expect(await redemptionVaultWithMToken.paused()).eq(false); + it('should fail: when function is paused', async () => { + const { + redemptionVaultWithMToken, + owner, + regularAccounts, + pauseManager, + } = await loadFixture(defaultDeploy); + await pauseVaultFn( + { pauseManager, owner }, + redemptionVaultWithMToken, + encodeFnSelector('setRedemptionVault(address)'), + ); + await setRedemptionVaultTest( + { vault: redemptionVaultWithMToken, owner }, + regularAccounts[0].address, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); - expect(await redemptionVaultWithMToken.tokensReceiver()).eq( - tokensReceiver.address, - ); - expect(await redemptionVaultWithMToken.feeReceiver()).eq( - feeReceiver.address, - ); + it('should succeed and emit SetRedemptionVault event', async () => { + const { redemptionVaultWithMToken, owner, regularAccounts } = + await loadFixture(defaultDeploy); - expect(await redemptionVaultWithMToken.minAmount()).eq(1000); - expect(await redemptionVaultWithMToken.minFiatRedeemAmount()).eq(1000); + const newVault = regularAccounts[0].address; - expect(await redemptionVaultWithMToken.instantFee()).eq('100'); + await expect(redemptionVaultWithMToken.setRedemptionVault(newVault)) + .to.emit(redemptionVaultWithMToken, 'SetRedemptionVault') + .withArgs(newVault); - expect(await redemptionVaultWithMToken.instantDailyLimit()).eq( - parseUnits('100000'), - ); + expect(await redemptionVaultWithMToken.redemptionVault()).eq( + newVault, + ); + }); + }); - expect(await redemptionVaultWithMToken.mTokenDataFeed()).eq( - mFoneToUsdDataFeed.address, - ); - expect(await redemptionVaultWithMToken.variationTolerance()).eq(1); + describe('checkAndRedeemMToken()', () => { + it('should not redeem when vault has sufficient tokenOut balance', async () => { + const { redemptionVaultWithMToken, stableCoins, mTokenLoan } = + await loadFixture(defaultDeploy); - expect(await redemptionVaultWithMToken.vaultRole()).eq( - roles.tokenRoles.mTBILL.redemptionVaultAdmin, - ); + await mintToken(stableCoins.dai, redemptionVaultWithMToken, 1000); + await mintToken(mTokenLoan, redemptionVaultWithMToken, 1000); - expect(await redemptionVaultWithMToken.MANUAL_FULLFILMENT_TOKEN()).eq( - ethers.constants.AddressZero, - ); + await expect( + redemptionVaultWithMToken.checkAndRedeemMToken( + stableCoins.dai.address, + parseUnits('500', 9), + parseUnits('1'), + ), + ).not.reverted; - expect(await redemptionVaultWithMToken.redemptionVault()).eq( - redemptionVault.address, - ); - }); - - it('failing deployment', async () => { - const { - mFONE, - tokensReceiver, - feeReceiver, - mFoneToUsdDataFeed, - accessControl, - mockedSanctionsList, - owner, - } = await loadFixture(defaultDeploy); - - const redemptionVaultWithMToken = - await new RedemptionVaultWithMTokenTest__factory(owner).deploy(); - - await expect( - redemptionVaultWithMToken[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address)' - ]( - accessControl.address, - { - mToken: mFONE.address, - mTokenDataFeed: mFoneToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - { - fiatAdditionalFee: 10000, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: parseUnits('100'), - }, - constants.AddressZero, - ), - ).to.be.reverted; - }); - - describe('initialization', () => { - it('should fail: call initialize() when already initialized', async () => { - const { redemptionVaultWithMToken } = await loadFixture(defaultDeploy); - - await expect( - redemptionVaultWithMToken[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address)' - ]( - constants.AddressZero, - { - mToken: constants.AddressZero, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: constants.AddressZero, - tokensReceiver: constants.AddressZero, - }, - { - instantFee: 0, - instantDailyLimit: 0, - }, - constants.AddressZero, - 0, - 0, - { - fiatAdditionalFee: 0, - fiatFlatFee: 0, - minFiatRedeemAmount: 0, - }, - constants.AddressZero, - constants.AddressZero, - ), - ).revertedWith('Initializable: contract is already initialized'); - }); + expect( + await stableCoins.dai.balanceOf(redemptionVaultWithMToken.address), + ).to.be.eq(parseUnits('1000', 9)); + expect( + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.eq(parseUnits('1000', 18)); + }); - it('should fail: call with initializing == false', async () => { - const { - owner, - accessControl, - mFONE, - tokensReceiver, - feeReceiver, - mFoneToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initializeWithoutInitializer( - accessControl.address, - { - mToken: mFONE.address, - mTokenDataFeed: mFoneToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('Initializable: contract is not initializing'); - }); + it('should redeem missing amount via mToken RV', async () => { + const { + redemptionVaultWithMToken, + stableCoins, + mTokenLoan, + owner, + dataFeed, + redemptionVaultLoanSwapper, + } = await loadFixture(defaultDeploy); - it('should fail: when redemptionVault address zero', async () => { - const { - owner, - accessControl, - mFONE, - tokensReceiver, - feeReceiver, - mFoneToUsdDataFeed, - mockedSanctionsList, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - const redemptionVaultWithMToken = - await new RedemptionVaultWithMTokenTest__factory(owner).deploy(); - - await expect( - redemptionVaultWithMToken[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address)' - ]( - accessControl.address, - { - mToken: mFONE.address, - mTokenDataFeed: mFoneToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, - }, - requestRedeemer.address, - constants.AddressZero, - ), - ).revertedWith('zero address'); - }); - }); + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); - describe('setRedemptionVault()', () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithMToken, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithMToken - .connect(regularAccounts[0]) - .setRedemptionVault(regularAccounts[1].address), - ).to.be.revertedWith('WMAC: hasnt role'); - }); + await mintToken(stableCoins.dai, redemptionVaultWithMToken, 500); + await mintToken(mTokenLoan, redemptionVaultWithMToken, 10000); + await mintToken( + stableCoins.dai, + redemptionVaultLoanSwapper, + 1_000_000, + ); - it('should fail: zero address', async () => { - const { redemptionVaultWithMToken } = await loadFixture(defaultDeploy); - await expect( - redemptionVaultWithMToken.setRedemptionVault(constants.AddressZero), - ).to.be.revertedWith('zero address'); - }); + const mTokenLoanBefore = await mTokenLoan.balanceOf( + redemptionVaultWithMToken.address, + ); - it('should fail: same address', async () => { - const { redemptionVaultWithMToken, redemptionVault } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithMToken.setRedemptionVault(redemptionVault.address), - ).to.be.revertedWith('RVMT: already set'); - }); + await redemptionVaultWithMToken.checkAndRedeemMToken( + stableCoins.dai.address, + parseUnits('1000', 9), + parseUnits('1'), + ); - it('should succeed and emit SetRedemptionVault event', async () => { - const { redemptionVaultWithMToken, owner, regularAccounts } = - await loadFixture(defaultDeploy); + const mTokenLoanAfter = await mTokenLoan.balanceOf( + redemptionVaultWithMToken.address, + ); + expect(mTokenLoanAfter).to.be.lt(mTokenLoanBefore); - const newVault = regularAccounts[0].address; + const daiAfter = await stableCoins.dai.balanceOf( + redemptionVaultWithMToken.address, + ); + expect(daiAfter).to.be.gte(parseUnits('1000', 9)); + }); - await expect(redemptionVaultWithMToken.setRedemptionVault(newVault)) - .to.emit(redemptionVaultWithMToken, 'SetRedemptionVault') - .withArgs(owner.address, newVault); + it('shouldnt revert when insufficient mTokenLoan balance', async () => { + const { + redemptionVaultWithMToken, + stableCoins, + owner, + dataFeed, + redemptionVaultLoanSwapper, + } = await loadFixture(defaultDeploy); - expect(await redemptionVaultWithMToken.redemptionVault()).eq(newVault); - }); - }); - - describe('setMinAmount()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithMToken, regularAccounts } = - await loadFixture(defaultDeploy); - await setMinAmountTest( - { vault: redemptionVaultWithMToken, owner }, - 10000, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMToken } = await loadFixture( - defaultDeploy, - ); - await setMinAmountTest( - { vault: redemptionVaultWithMToken, owner }, - 10000, - ); - }); - }); - - describe('setMinFiatRedeemAmount()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithMToken, regularAccounts } = - await loadFixture(defaultDeploy); - await setMinFiatRedeemAmountTest( - { redemptionVault: redemptionVaultWithMToken, owner }, - 10000, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMToken } = await loadFixture( - defaultDeploy, - ); - await setMinFiatRedeemAmountTest( - { redemptionVault: redemptionVaultWithMToken, owner }, - 10000, - ); - }); - }); - - describe('setFiatAdditionalFee()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithMToken, regularAccounts } = - await loadFixture(defaultDeploy); - await setFiatAdditionalFeeTest( - { redemptionVault: redemptionVaultWithMToken, owner }, - 100, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMToken } = await loadFixture( - defaultDeploy, - ); - await setFiatAdditionalFeeTest( - { redemptionVault: redemptionVaultWithMToken, owner }, - 100, - ); - }); - }); - - describe('setInstantDailyLimit()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithMToken, regularAccounts } = - await loadFixture(defaultDeploy); - await setInstantDailyLimitTest( - { vault: redemptionVaultWithMToken, owner }, - parseUnits('1000'), - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMToken } = await loadFixture( - defaultDeploy, - ); - await setInstantDailyLimitTest( - { vault: redemptionVaultWithMToken, owner }, - parseUnits('1000'), - ); - }); - it('should fail: when limit is zero', async () => { - const { owner, redemptionVaultWithMToken } = await loadFixture( - defaultDeploy, - ); - await setInstantDailyLimitTest( - { vault: redemptionVaultWithMToken, owner }, - constants.Zero, - { revertMessage: 'MV: limit zero' }, - ); - }); - }); - - describe('addPaymentToken()', () => { - it('should fail: call from address without vault admin role', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - undefined, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMToken, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - }); - }); - - describe('removePaymentToken()', () => { - it('should fail: call from address without vault admin role', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await removePaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMToken, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await removePaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - ); - }); - }); - - describe('addWaivedFeeAccount()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithMToken, regularAccounts } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithMToken, owner }, - regularAccounts[0].address, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMToken, regularAccounts } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithMToken, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('removeWaivedFeeAccount()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithMToken, regularAccounts } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithMToken, owner }, - regularAccounts[0].address, - ); - await removeWaivedFeeAccountTest( - { vault: redemptionVaultWithMToken, owner }, - regularAccounts[0].address, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMToken, regularAccounts } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithMToken, owner }, - regularAccounts[0].address, - ); - await removeWaivedFeeAccountTest( - { vault: redemptionVaultWithMToken, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('setFee()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithMToken, regularAccounts } = - await loadFixture(defaultDeploy); - await setInstantFeeTest( - { vault: redemptionVaultWithMToken, owner }, - 100, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMToken } = await loadFixture( - defaultDeploy, - ); - await setInstantFeeTest({ vault: redemptionVaultWithMToken, owner }, 100); - }); - }); - - describe('setVariabilityTolerance()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithMToken, regularAccounts } = - await loadFixture(defaultDeploy); - await setVariabilityToleranceTest( - { vault: redemptionVaultWithMToken, owner }, - 100, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMToken } = await loadFixture( - defaultDeploy, - ); - await setVariabilityToleranceTest( - { vault: redemptionVaultWithMToken, owner }, - 100, - ); - }); - }); - - describe('withdrawToken()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithMToken, stableCoins, regularAccounts } = - await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, redemptionVaultWithMToken, 1); - await withdrawTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - 1, - owner, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMToken, stableCoins } = - await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, redemptionVaultWithMToken, 100); - await withdrawTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - 100, - owner, - ); - }); - }); + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); - describe('freeFromMinAmount()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithMToken, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithMToken - .connect(regularAccounts[0]) - .freeFromMinAmount(regularAccounts[1].address, true), - ).to.be.revertedWith('WMAC: hasnt role'); - }); + await expect( + redemptionVaultWithMToken.checkAndRedeemMToken( + stableCoins.dai.address, + parseUnits('1000', 9), + parseUnits('1'), + ), + ).to.not.be.reverted; + }); + }); - it('should not fail', async () => { - const { redemptionVaultWithMToken, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithMToken.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.not.reverted; - - expect( - await redemptionVaultWithMToken.isFreeFromMinAmount( - regularAccounts[0].address, - ), - ).to.eq(true); - }); - }); - - describe('changeTokenFee()', () => { - it('should fail: call from address without vault admin role', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai.address, - 100, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMToken, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai.address, - 100, - ); - }); - }); - - describe('changeTokenAllowance()', () => { - it('should fail: call from address without vault admin role', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai.address, - 100, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMToken, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai.address, - 100, - ); - }); - }); - - describe('checkAndRedeemMToken()', () => { - it('should not redeem mTBILL when vault has sufficient tokenOut balance', async () => { - const { - redemptionVaultWithMToken, - stableCoins, - mTBILL, - owner, - dataFeed, - redemptionVault, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); + describe('redeemInstant()', () => { + describe('preferLoanLiquidity=true', () => { + it('redeem 100 mTBILL, when vault has enough DAI and all fees are 0', async () => { + const { + owner, + redemptionVaultWithMToken, + stableCoins, + mTokenLoan, + mTBILL, + mTokenLoanToUsdDataFeed, + mTokenToUsdDataFeed, + dataFeed, + mockedAggregator, + } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, redemptionVaultWithMToken, 1000); - await mintToken(mTBILL, redemptionVaultWithMToken, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); + await setRoundData({ mockedAggregator }, 1); + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithMToken, owner }, + true, + ); - await redemptionVaultWithMToken.checkAndRedeemMToken( - stableCoins.dai.address, - parseUnits('500', 9), - parseUnits('1'), - ); + await mintToken(mTBILL, owner, 100_000); + await mintToken( + stableCoins.dai, + redemptionVaultWithMToken, + 1_000_000, + ); + await mintToken(mTokenLoan, redemptionVaultWithMToken, 100_000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 100_000, + ); - const mTbillAfter = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - expect(mTbillAfter).to.equal(mTbillBefore); - }); + await setInstantFeeTest( + { vault: redemptionVaultWithMToken, owner }, + 0, + ); - it('should redeem missing amount via mToken RV', async () => { - const { - redemptionVaultWithMToken, - stableCoins, - mTBILL, - owner, - dataFeed, - redemptionVault, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); + await redeemInstantWithMTokenTest( + { + redemptionVaultWithMToken, + owner, + mTokenLoan, + mTBILL, + mTokenToUsdDataFeed, + mTokenLoanToUsdDataFeed, + }, + stableCoins.dai, + 100, + ); + }); - await mintToken(stableCoins.dai, redemptionVaultWithMToken, 500); - await mintToken(mTBILL, redemptionVaultWithMToken, 10000); - await mintToken(stableCoins.dai, redemptionVault, 1_000_000); + it('when vault has no DAI but has mToken sleeve and LP liquidity, LP liquidity should be used first', async () => { + const { + owner, + redemptionVaultWithMToken, + stableCoins, + mTokenLoan, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + mockedAggregator, + mockedAggregatorMTokenLoan, + redemptionVaultLoanSwapper, + loanLp, + } = await loadFixture(defaultDeploy); - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); - await redemptionVaultWithMToken.checkAndRedeemMToken( - stableCoins.dai.address, - parseUnits('1000', 9), - parseUnits('1'), - ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData( + { mockedAggregator: mockedAggregatorMTokenLoan }, + 1, + ); + await setInstantFeeTest( + { vault: redemptionVaultWithMToken, owner }, + 0, + ); + await setInstantFeeTest( + { vault: redemptionVaultLoanSwapper, owner }, + 0, + ); + await setLoanLpTest( + { redemptionVault: redemptionVaultWithMToken, owner }, + loanLp.address, + ); + await setLoanSwapperVaultTest( + { redemptionVault: redemptionVaultWithMToken, owner }, + redemptionVaultLoanSwapper.address, + ); + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithMToken, owner }, + true, + ); - const mTbillAfter = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - expect(mTbillAfter).to.be.lt(mTbillBefore); + await mintToken(mTBILL, owner, 100_000); + await mintToken(mTokenLoan, redemptionVaultWithMToken, 100_000); + await mintToken(mTokenLoan, loanLp, 100_000); + await mintToken( + stableCoins.dai, + redemptionVaultLoanSwapper, + 1_000_000, + ); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 100_000, + ); + await approveBase18( + loanLp, + mTokenLoan, + redemptionVaultWithMToken, + 100_000, + ); - const daiAfter = await stableCoins.dai.balanceOf( - redemptionVaultWithMToken.address, - ); - expect(daiAfter).to.be.gte(parseUnits('1000', 9)); - }); + const loanLpBalanceBefore = await mTokenLoan.balanceOf( + loanLp.address, + ); - it('should revert when insufficient mTBILL balance', async () => { - const { - redemptionVaultWithMToken, - stableCoins, - owner, - dataFeed, - redemptionVault, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await expect( - redemptionVaultWithMToken.checkAndRedeemMToken( - stableCoins.dai.address, - parseUnits('1000', 9), - parseUnits('1'), - ), - ).to.be.revertedWith('RVMT: balance < needed'); - }); - }); - - describe('redeemInstant()', () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 0, - { - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: call is paused', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemInstant(address,uint256,uint256)', - ); - await pauseVaultFn(redemptionVaultWithMToken, selector); - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: when user has no mFONE allowance', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(mFONE, owner, 100_000); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: when user has no mFONE balance', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: burn amount exceeds balance', - }, - ); - }); - - it('should fail: when data feed rate is 0', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - mockedAggregatorMFone, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(mFONE, owner, 100_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - await setRoundData({ mockedAggregator: mockedAggregatorMFone }, 0); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: when amount < minAmount', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(mFONE, owner, 100_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - - await setMinAmountTest( - { vault: redemptionVaultWithMToken, owner }, - 100_000, - ); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 999, - { - revertMessage: 'RV: amount < min', - }, - ); - }); - - it('should fail: when token allowance exceeded', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - mockedAggregator, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(mFONE, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVaultWithMToken, 1_000_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai.address, - 100, - ); - await setRoundData({ mockedAggregator }, 4); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - - it('should fail: when daily limit exceeded', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - mockedAggregator, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(mFONE, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVaultWithMToken, 1_000_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - await setInstantDailyLimitTest( - { vault: redemptionVaultWithMToken, owner }, - parseUnits('1000'), - ); - await setRoundData({ mockedAggregator }, 4); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed limit', - }, - ); - }); - - it('should fail: when fee is 100%', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(mFONE, owner, 100_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - await setInstantFeeTest( - { vault: redemptionVaultWithMToken, owner }, - 10000, - ); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'RV: amountMTokenIn < fee', - }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithMToken.setGreenlistEnable(true); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user is blacklisted', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user is sanctioned', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - mockedSanctionsList, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: user try to instant redeem fiat', async () => { - const { - owner, - redemptionVaultWithMToken, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mFONE, owner, 100_000); - await mintToken(mTBILL, redemptionVaultWithMToken, 100_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - await redemptionVaultWithMToken.MANUAL_FULLFILMENT_TOKEN(), - 99_999, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when inner vault fee is not waived', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - redemptionVault, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - // Remove the waived fee — inner vault will charge fee on this contract - await redemptionVault.removeWaivedFeeAccount( - redemptionVaultWithMToken.address, - ); - - await mintToken(mFONE, owner, 100_000); - await mintToken(mTBILL, redemptionVaultWithMToken, 100_000); - await mintToken(stableCoins.dai, redemptionVault, 1_000_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - - // No DAI on vault — forces mTBILL redemption path where inner vault fee causes revert - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'RVMT: fees not waived on target', - }, - ); - }); - - it('should fail: vault has no mTBILL and no DAI', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - redemptionVault, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await mintToken(mFONE, owner, 100_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'RVMT: balance < needed', - }, - ); - }); - - it('redeem 100 mFONE, when vault has enough DAI and all fees are 0', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - mockedAggregator, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mFONE, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVaultWithMToken, 1_000_000); - await mintToken(mTBILL, redemptionVaultWithMToken, 100_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - - await setInstantFeeTest({ vault: redemptionVaultWithMToken, owner }, 0); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem 100 mFONE, when vault has enough DAI (with fees)', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - mockedAggregator, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mFONE, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVaultWithMToken, 1_000_000); - await mintToken(mTBILL, redemptionVaultWithMToken, 100_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem 100 mFONE, vault has no DAI => triggers mTBILL redemption', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - redemptionVault, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await mintToken(mFONE, owner, 100_000); - await mintToken(mTBILL, redemptionVaultWithMToken, 100_000); - await mintToken(stableCoins.dai, redemptionVault, 1_000_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - useMTokenSleeve: true, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem 100 mFONE, vault has partial DAI => triggers partial mTBILL redemption', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - redemptionVault, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await mintToken(mFONE, owner, 100_000); - await mintToken(mTBILL, redemptionVaultWithMToken, 100_000); - await mintToken(stableCoins.dai, redemptionVaultWithMToken, 10); - await mintToken(stableCoins.dai, redemptionVault, 1_000_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - useMTokenSleeve: true, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem 100 mFONE with divergent rates (mFONE=$5, mTBILL=$2) => triggers mTBILL redemption', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - redemptionVault, - mockedAggregatorMFone, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator: mockedAggregatorMFone }, 5); - - await mintToken(mFONE, owner, 100_000); - await mintToken(mTBILL, redemptionVaultWithMToken, 100_000); - await mintToken(stableCoins.dai, redemptionVault, 1_000_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - useMTokenSleeve: true, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem with waived fee', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mFONE, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - dataFeed, - mockedAggregator, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithMToken, owner }, - owner.address, - ); - - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mFONE, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVaultWithMToken, 1_000_000); - await mintToken(mTBILL, redemptionVaultWithMToken, 100_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - waivedFee: true, - }, - stableCoins.dai, - 100, - ); - }); - it('redeem 100 mFONE (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - mFoneToUsdDataFeed, - mFONE, - mTBILL, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVaultWithMToken, 100000); - await mintToken(mTBILL, redemptionVaultWithMToken, 100000); - await mintToken(mFONE, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mFONE, - redemptionVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - mFoneToUsdDataFeed, - mFONE, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mFONE when other fn overload is paused (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - mFoneToUsdDataFeed, - mFONE, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithMToken, - encodeFnSelector('redeemInstant(address,uint256,uint256)'), - ); - await mintToken(stableCoins.dai, redemptionVaultWithMToken, 100000); - await mintToken(mTBILL, redemptionVaultWithMToken, 100000); - await mintToken(mFONE, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mFONE, - redemptionVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - mFoneToUsdDataFeed, - mFONE, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mFONE when other fn overload is paused', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - mFoneToUsdDataFeed, - mFONE, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithMToken, - encodeFnSelector('redeemInstant(address,uint256,uint256,address)'), - ); - await mintToken(stableCoins.dai, redemptionVaultWithMToken, 100000); - await mintToken(mTBILL, redemptionVaultWithMToken, 100000); - await mintToken(mFONE, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mFONE, - redemptionVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - mFoneToUsdDataFeed, - mFONE, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('should fail: when function with custom recipient is paused', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - mFoneToUsdDataFeed, - mFONE, - } = await loadFixture(defaultDeploy); - await mintToken(mFONE, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithMToken, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemInstant(address,uint256,uint256,address)', - ); - await pauseVaultFn(redemptionVaultWithMToken, selector); - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - mFoneToUsdDataFeed, - mFONE, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - mFoneToUsdDataFeed, - mFONE, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithMToken.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - mFoneToUsdDataFeed, - mFONE, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - mFoneToUsdDataFeed, - mFONE, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - mFoneToUsdDataFeed, - mFONE, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - mFoneToUsdDataFeed, - mFONE, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await redeemInstantWithMTokenTest( - { - redemptionVaultWithMToken, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - mFoneToUsdDataFeed, - mFONE, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - }); - - describe('redeemRequest()', () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithMToken, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithMToken, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - stableCoins.dai, - 0, - { - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: call is paused', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await pauseVaultFn( - redemptionVaultWithMToken, - encodeFnSelector('redeemRequest(address,uint256)'), - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithMToken, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: when user has no mFONE balance', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithMToken, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('redeem request 100 mFONE', async () => { - const { - owner, - redemptionVaultWithMToken, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(mFONE, owner, 100_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithMToken, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - }); - - describe('redeemFiatRequest()', () => { - it('should fail: call is paused', async () => { - const { owner, redemptionVaultWithMToken, mFONE, mFoneToUsdDataFeed } = - await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithMToken, - encodeFnSelector('redeemFiatRequest(uint256)'), - ); - - await redeemFiatRequestTest( - { - redemptionVault: redemptionVaultWithMToken, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - 100, - { - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('redeem fiat request 100 mFONE', async () => { - const { owner, redemptionVaultWithMToken, mFONE, mFoneToUsdDataFeed } = - await loadFixture(defaultDeploy); - - await mintToken(mFONE, owner, 100_000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100_000); - - await redeemFiatRequestTest( - { - redemptionVault: redemptionVaultWithMToken, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - 100, - ); - }); - }); - - describe('approveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithMToken: redemptionVault, - regularAccounts, - mFoneToUsdDataFeed, - mFONE, - } = await loadFixture(defaultDeploy); - await approveRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithMToken: redemptionVault, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await approveRedeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMFone, - redemptionVaultWithMToken: redemptionVault, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - - await mintToken(mFONE, owner, 100); - await approveBase18(owner, mFONE, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMFone }, 5); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRedeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - +requestId, - parseUnits('1'), - ); - await approveRedeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - +requestId, - parseUnits('1'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMFone, - redemptionVaultWithMToken: redemptionVault, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - - await mintToken(mFONE, owner, 100); - await approveBase18(owner, mFONE, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMFone }, 5); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRedeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - +requestId, - parseUnits('1'), - ); - }); - }); - - describe('approveRequest() with fiat', async () => { - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMFone, - redemptionVaultWithMToken: redemptionVault, - mFONE, - mFoneToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mFONE, owner, 100); - await approveBase18(owner, mFONE, redemptionVault, 100); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMFone }, 5); - - await redeemFiatRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - 100, - ); - const requestId = 0; - await changeTokenAllowanceTest( - { vault: redemptionVault, owner }, - constants.AddressZero, - parseUnits('100'), - ); - - await approveRedeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - +requestId, - parseUnits('1'), - ); - }); - }); - - describe('safeApproveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithMToken: redemptionVault, - regularAccounts, - mFoneToUsdDataFeed, - mFONE, - } = await loadFixture(defaultDeploy); - await safeApproveRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithMToken: redemptionVault, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeApproveRedeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: if new rate greater then variabilityTolerance', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMFone, - redemptionVaultWithMToken: redemptionVault, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mFONE, owner, 100); - await approveBase18(owner, mFONE, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMFone }, 5); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - +requestId, - parseUnits('6'), - { revertMessage: 'MV: exceed price diviation' }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMFone, - redemptionVaultWithMToken: redemptionVault, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mFONE, owner, 100); - await approveBase18(owner, mFONE, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMFone }, 5); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - +requestId, - parseUnits('5.000001'), - ); - await safeApproveRedeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - +requestId, - parseUnits('5.00001'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('safe approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMFone, - redemptionVaultWithMToken: redemptionVault, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mFONE, owner, 100); - await approveBase18(owner, mFONE, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMFone }, 5); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - +requestId, - parseUnits('5.000001'), - ); - }); - }); - - describe('rejectRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithMToken: redemptionVault, - regularAccounts, - mFoneToUsdDataFeed, - mFONE, - } = await loadFixture(defaultDeploy); - await rejectRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithMToken: redemptionVault, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await rejectRedeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - 1, - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMFone, - redemptionVaultWithMToken: redemptionVault, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mFONE, owner, 100); - await approveBase18(owner, mFONE, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMFone }, 5); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRedeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - +requestId, - ); - await rejectRedeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - +requestId, - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('reject request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMFone, - redemptionVaultWithMToken: redemptionVault, - stableCoins, - mFONE, - mFoneToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mFONE, owner, 100); - await approveBase18(owner, mFONE, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMFone }, 5); - - await redeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRedeemRequestTest( - { - redemptionVault, - owner, - mTBILL: mFONE, - mTokenToUsdDataFeed: mFoneToUsdDataFeed, - }, - +requestId, - ); - }); - }); - - describe('ceiling division math correctness', () => { - const TOKEN_DECIMALS: Record = { - usdc6: 6, - usdc: 8, - dai: 9, - usdt: 18, - }; - - /** - * Mirrors the Solidity math in RedemptionVaultWithMToken._redeemInstant: - * amountTokenOutWithoutFee = truncate( - * (amountMTokenWithoutFee * mTokenRate / tokenOutRate), tokenDecimals - * ) - * Returns the expected user output in NATIVE decimals. - */ - function computeExpectedTokenOut( - amountMTokenIn: BigNumber, - mTokenRate: BigNumber, - tokenOutRate: BigNumber, - tokenDecimals: number, - feePercent: number, - isWaived: boolean, - ): BigNumber { - const fee = isWaived - ? BigNumber.from(0) - : amountMTokenIn.mul(feePercent).div(10000); - const amountWithoutFee = amountMTokenIn.sub(fee); - const base18Out = amountWithoutFee.mul(mTokenRate).div(tokenOutRate); - const scale = BigNumber.from(10).pow(18 - tokenDecimals); - const truncated = base18Out.div(scale); - return truncated; - } - - /** - * Lean helper: configures both outer + inner vault for a given tokenOut, - * sets rates, mints tokens, and returns everything needed to call redeemInstant. - * The outer vault has ZERO tokenOut to force the inner-vault redemption path. - */ - async function setupCeilDivTest(opts: { - fixture: Awaited>; - tokenKey: 'usdc6' | 'usdc' | 'dai' | 'usdt'; - mTbillRate: number; - isStable: boolean; - tokenOutRate?: number; - redeemAmount?: number; - }) { - const { - redemptionVaultWithMToken, - redemptionVault, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - mockedAggregatorMToken, - mockedAggregatorMFone, - mockedAggregator, - dataFeed, - stableCoins, - } = opts.fixture; - - const token = stableCoins[opts.tokenKey]; - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - token, - dataFeed.address, - 0, - opts.isStable, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - token, - dataFeed.address, - 0, - opts.isStable, - ); - - await setRoundData( - { mockedAggregator: mockedAggregatorMToken }, - opts.mTbillRate, - ); - if (opts.tokenOutRate !== undefined) { - await setRoundData({ mockedAggregator }, opts.tokenOutRate); - } - - await setInstantFeeTest({ vault: redemptionVaultWithMToken, owner }, 0); - - const amount = opts.redeemAmount ?? 100; - - await mintToken(mFONE, owner, amount * 100); - await mintToken(mTBILL, redemptionVaultWithMToken, amount * 100); - await mintToken(token, redemptionVault, amount * 10000); - await approveBase18( - owner, - mFONE, - redemptionVaultWithMToken, - amount * 100, - ); - - const mFoneRate = await mFoneToUsdDataFeed.getDataInBase18(); - const tokenOutRate = opts.isStable - ? parseUnits('1') - : await dataFeed.getDataInBase18(); - - return { - token, - amount, - redemptionVaultWithMToken, - redemptionVault, - owner, - mTBILL, - mFONE, - mFoneToUsdDataFeed, - mTokenToUsdDataFeed, - mockedAggregatorMFone, - mockedAggregator, - mFoneRate, - tokenOutRate, - }; - } - - describe('with base RedemptionVault as inner vault', () => { - const tokenVariants: Array<{ - key: 'usdc6' | 'usdc' | 'dai' | 'usdt'; - label: string; - }> = [ - { key: 'usdc6', label: '6-dec' }, - { key: 'usdc', label: '8-dec' }, - { key: 'dai', label: '9-dec' }, - { key: 'usdt', label: '18-dec' }, - ]; - - for (const { key, label } of tokenVariants) { - describe(`tokenOut ${label} (${key})`, () => { - it('succeeds with exact division (mTBILL=5, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - amount, - redemptionVaultWithMToken, - owner, - mTBILL, - mFoneRate, - tokenOutRate, - } = await setupCeilDivTest({ - fixture, - tokenKey: key, - mTbillRate: 5, - isStable: true, - }); - - const mTbillBefore = await mTBILL.balanceOf( + const sleeveBalanceBefore = await mTokenLoan.balanceOf( redemptionVaultWithMToken.address, ); - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - - const expected = computeExpectedTokenOut( - parseUnits(amount.toString()), - mFoneRate, - tokenOutRate, - TOKEN_DECIMALS[key], - 0, - true, - ); - const userTokenAfter = await token.balanceOf(owner.address); - expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); - }); - - it('succeeds with remainder-producing rate (mTBILL=1.05, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - amount, - redemptionVaultWithMToken, - owner, - mTBILL, - mFoneRate, - tokenOutRate, - } = await setupCeilDivTest({ - fixture, - tokenKey: key, - mTbillRate: 1.05, - isStable: true, - }); - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return ( + await mTokenLoan.balanceOf( + redemptionVaultWithMToken.address, + ) + ).div(10 ** (await stableCoins.dai.decimals())); + }, + }, + stableCoins.dai, + 100, ); - const userTokenBefore = await token.balanceOf(owner.address); - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - - const expected = computeExpectedTokenOut( - parseUnits(amount.toString()), - mFoneRate, - tokenOutRate, - TOKEN_DECIMALS[key], - 0, - true, + const loanLpBalanceAfter = await mTokenLoan.balanceOf( + loanLp.address, ); - const userTokenAfter = await token.balanceOf(owner.address); - expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); - }); - - it('succeeds with high mTBILL rate (mTBILL=100, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - amount, - redemptionVaultWithMToken, - owner, - mTBILL, - mFoneRate, - tokenOutRate, - } = await setupCeilDivTest({ - fixture, - tokenKey: key, - mTbillRate: 100, - isStable: true, - }); - - const mTbillBefore = await mTBILL.balanceOf( + const sleeveBalanceAfter = await mTokenLoan.balanceOf( redemptionVaultWithMToken.address, ); - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - - const expected = computeExpectedTokenOut( - parseUnits(amount.toString()), - mFoneRate, - tokenOutRate, - TOKEN_DECIMALS[key], - 0, - true, - ); - const userTokenAfter = await token.balanceOf(owner.address); - expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); + expect(sleeveBalanceAfter).to.be.lte(sleeveBalanceBefore); + expect(loanLpBalanceAfter).to.be.lt(loanLpBalanceBefore); }); - it('succeeds with low mTBILL rate (mTBILL=0.5, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); + it('redeem 100 mTBILL, vault has no DAI but has LP liquidity', async () => { const { - token, - amount, - redemptionVaultWithMToken, owner, - mTBILL, - mFoneRate, - tokenOutRate, - } = await setupCeilDivTest({ - fixture, - tokenKey: key, - mTbillRate: 0.5, - isStable: true, - }); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - - const expected = computeExpectedTokenOut( - parseUnits(amount.toString()), - mFoneRate, - tokenOutRate, - TOKEN_DECIMALS[key], - 0, - true, - ); - const userTokenAfter = await token.balanceOf(owner.address); - expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); - }); - - it('succeeds with near-boundary rate (mTBILL=1.00000003, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, redemptionVaultWithMToken, - owner, + stableCoins, + mTokenLoan, mTBILL, - mFoneRate, - tokenOutRate, - } = await setupCeilDivTest({ - fixture, - tokenKey: key, - mTbillRate: 1.00000003, - isStable: true, - redeemAmount: 10000, - }); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits('10000'), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); + mTokenToUsdDataFeed, + dataFeed, + mockedAggregator, + redemptionVaultLoanSwapper, + loanLp, + } = await loadFixture(defaultDeploy); - const expected = computeExpectedTokenOut( - parseUnits('10000'), - mFoneRate, - tokenOutRate, - TOKEN_DECIMALS[key], + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, 0, true, ); - const userTokenAfter = await token.balanceOf(owner.address); - expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); - }); - - it('succeeds with non-stable tokenOut (mTBILL=1.05, tokenOut=1.03)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - amount, - redemptionVaultWithMToken, - owner, - mTBILL, - mFoneRate, - tokenOutRate, - } = await setupCeilDivTest({ - fixture, - tokenKey: key, - mTbillRate: 1.05, - isStable: false, - tokenOutRate: 1.03, - }); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - - const expected = computeExpectedTokenOut( - parseUnits(amount.toString()), - mFoneRate, - tokenOutRate, - TOKEN_DECIMALS[key], + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, 0, true, ); - const userTokenAfter = await token.balanceOf(owner.address); - expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); - }); - it('succeeds with small redeem (1 mFONE, mTBILL=1.05, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - redemptionVaultWithMToken, + await setRoundData({ mockedAggregator }, 1); + await setLoanLpTest( + { redemptionVault: redemptionVaultWithMToken, owner }, + loanLp.address, + ); + await setLoanSwapperVaultTest( + { redemptionVault: redemptionVaultWithMToken, owner }, + redemptionVaultLoanSwapper.address, + ); + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithMToken, owner }, + true, + ); + + await mintToken(mTBILL, owner, 100_000); + await mintToken(mTokenLoan, loanLp, 100_000); + await mintToken( + stableCoins.dai, + redemptionVaultLoanSwapper, + 1_000_000, + ); + await approveBase18( owner, mTBILL, - mFoneRate, - tokenOutRate, - } = await setupCeilDivTest({ - fixture, - tokenKey: key, - mTbillRate: 1.05, - isStable: true, - redeemAmount: 1, - }); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, + redemptionVaultWithMToken, + 100_000, + ); + await approveBase18( + loanLp, + mTokenLoan, + redemptionVaultWithMToken, + 100_000, ); - const userTokenBefore = await token.balanceOf(owner.address); - await setMinAmountTest( + await setInstantFeeTest( { vault: redemptionVaultWithMToken, owner }, 0, ); - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits('1'), - 0, - ); + const loanLpBalanceBefore = await mTokenLoan.balanceOf( + loanLp.address, + ); - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 100, + ); - const expected = computeExpectedTokenOut( - parseUnits('1'), - mFoneRate, - tokenOutRate, - TOKEN_DECIMALS[key], - 0, - true, + const loanLpBalanceAfter = await mTokenLoan.balanceOf( + loanLp.address, ); - const userTokenAfter = await token.balanceOf(owner.address); - expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); + expect(loanLpBalanceAfter).to.be.lt(loanLpBalanceBefore); }); - it('succeeds with large redeem near daily limit (mTBILL=1.05, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); + it('when vault has enough DAI and enough LP liquidity, LP should be used first', async () => { const { - token, + owner, + redemptionVaultWithMToken, + stableCoins, + mTokenLoan, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + mockedAggregator, + redemptionVaultLoanSwapper, + loanLp, + } = await loadFixture(defaultDeploy); + + await mintToken( + stableCoins.dai, + redemptionVaultLoanSwapper, + 1_000_000, + ); + await mintToken( + stableCoins.dai, + redemptionVaultWithMToken, + 1_000_000, + ); + await mintToken(mTokenLoan, loanLp, 100_000); + await approveBase18( + loanLp, + mTokenLoan, redemptionVaultWithMToken, + 100_000, + ); + + await mintToken(mTBILL, owner, 100_000); + await approveBase18( owner, mTBILL, - mFoneRate, - tokenOutRate, - } = await setupCeilDivTest({ - fixture, - tokenKey: key, - mTbillRate: 1.05, - isStable: true, - redeemAmount: 50000, - }); + redemptionVaultWithMToken, + 100_000, + ); + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest( + { vault: redemptionVaultWithMToken, owner }, + 0, + ); + await setRoundData({ mockedAggregator }, 1); + await setLoanLpTest( + { redemptionVault: redemptionVaultWithMToken, owner }, + loanLp.address, + ); + await setLoanSwapperVaultTest( + { redemptionVault: redemptionVaultWithMToken, owner }, + redemptionVaultLoanSwapper.address, + ); + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithMToken, owner }, + true, + ); - const mTbillBefore = await mTBILL.balanceOf( + const loanLpBalanceBefore = await mTokenLoan.balanceOf( + loanLp.address, + ); + const vaultDaiBalanceBefore = await stableCoins.dai.balanceOf( redemptionVaultWithMToken.address, ); - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits('50000'), - 0, - ); - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 100, + ); - const expected = computeExpectedTokenOut( - parseUnits('50000'), - mFoneRate, - tokenOutRate, - TOKEN_DECIMALS[key], - 0, - true, + const loanLpBalanceAfter = await mTokenLoan.balanceOf( + loanLp.address, ); - const userTokenAfter = await token.balanceOf(owner.address); - expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); + const vaultDaiBalanceAfter = await stableCoins.dai.balanceOf( + redemptionVaultWithMToken.address, + ); + expect(loanLpBalanceAfter).to.be.lt(loanLpBalanceBefore); + expect(vaultDaiBalanceAfter).to.be.eq(vaultDaiBalanceBefore); }); - }); - } - }); - describe('with RedemptionVaultWithAave as inner vault', () => { - for (const { key, label } of [ - { key: 'usdc' as const, label: '8-dec' }, - { key: 'usdc6' as const, label: '6-dec' }, - ]) { - describe(`tokenOut ${label} (${key})`, () => { - async function setupAaveInnerVault( - fixture: Awaited>, - tokenKey: 'usdc' | 'usdc6', - ) { + it('when vault partially has USDC, partially has mToken sleeve and partially has LP liquidity, so all the liquidity should be used', async () => { const { - redemptionVaultWithMToken, - redemptionVaultWithAave, - aavePoolMock, - aUSDC, owner, + redemptionVaultWithMToken, + stableCoins, + mTokenLoan, mTBILL, - mFONE, - mFoneToUsdDataFeed, mTokenToUsdDataFeed, - mockedAggregatorMToken, - mockedAggregator, dataFeed, - stableCoins, - } = fixture; - - const token = stableCoins[tokenKey]; + mockedAggregator, + redemptionVaultLoanSwapper, + loanLp, + redemptionVault, + mockedAggregatorMToken, + } = await loadFixture(defaultDeploy); - await redemptionVaultWithMToken.setRedemptionVault( - redemptionVaultWithAave.address, + await mintToken(stableCoins.dai, redemptionVaultLoanSwapper, 400); + await mintToken(stableCoins.dai, redemptionVaultWithMToken, 300); + await mintToken(stableCoins.dai, redemptionVault, 300); + await mintToken(mTokenLoan, loanLp, 400); + await mintToken(mTBILL, redemptionVaultWithMToken, 300); + await approveBase18( + loanLp, + mTokenLoan, + redemptionVaultWithMToken, + 400, ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithMToken, 1000); await addPaymentTokenTest( { vault: redemptionVaultWithMToken, owner }, - token, + stableCoins.dai, dataFeed.address, 0, true, ); await addPaymentTokenTest( - { vault: redemptionVaultWithAave, owner }, - token, + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, dataFeed.address, 0, true, ); - await redemptionVaultWithAave.addWaivedFeeAccount( - redemptionVaultWithMToken.address, + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, ); - - if (tokenKey === 'usdc6') { - const { ERC20Mock__factory } = await import( - '../../typechain-types' - ); - const aUsdc6 = await new ERC20Mock__factory(owner).deploy(6); - await aavePoolMock.setReserveAToken( - token.address, - aUsdc6.address, - ); - await token.mint(aavePoolMock.address, parseUnits('1000000', 6)); - await aUsdc6.mint( - redemptionVaultWithAave.address, - parseUnits('1000000', 6), - ); - await redemptionVaultWithAave.setAavePool( - token.address, - aavePoolMock.address, - ); - } else { - await token.mint(aavePoolMock.address, parseUnits('1000000')); - await aUSDC.mint( - redemptionVaultWithAave.address, - parseUnits('1000000'), - ); - } - await setInstantFeeTest( { vault: redemptionVaultWithMToken, owner }, 0, ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - return { - token, + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithMToken, owner }, + true, + ); + + await redemptionVaultWithMToken.setRedemptionVault( + redemptionVault.address, + ); + + await redemptionVault.setWaivedFeeAccount( + redemptionVaultWithMToken.address, + true, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + checkSupply: false, + additionalLiquidity: async () => { + return ( + await mTBILL.balanceOf(redemptionVaultWithMToken.address) + ).div(10 ** (await stableCoins.dai.decimals())); + }, + }, + stableCoins.dai, + 1000, + ); + + expect(await mTokenLoan.balanceOf(loanLp.address)).to.eq(0); + expect( + await stableCoins.dai.balanceOf( + redemptionVaultWithMToken.address, + ), + ).to.be.eq(0); + expect( + await stableCoins.dai.balanceOf(redemptionVault.address), + ).to.be.eq(0); + expect( + await mTBILL.balanceOf(redemptionVaultWithMToken.address), + ).to.be.eq(0); + }); + }); + + it('should fail: when inner vault fee is not waived', async () => { + const { + owner, + redemptionVaultWithMToken, + stableCoins, + mTokenLoan, + mTBILL, + mTokenLoanToUsdDataFeed, + mTokenToUsdDataFeed, + dataFeed, + redemptionVaultLoanSwapper, + } = await loadFixture(defaultDeploy); + + await setLoanLpTest( + { redemptionVault: redemptionVaultWithMToken, owner }, + constants.AddressZero, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + // Remove the waived fee — inner vault will charge fee on this contract + await redemptionVaultLoanSwapper.setWaivedFeeAccount( + redemptionVaultWithMToken.address, + false, + ); + + await mintToken(mTBILL, owner, 100_000); + await mintToken(mTokenLoan, redemptionVaultWithMToken, 100_000); + await mintToken( + stableCoins.dai, + redemptionVaultLoanSwapper, + 1_000_000, + ); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 100_000, + ); + + // No DAI on vault — forces mTokenLoan redemption path where inner vault fee causes revert + await redeemInstantWithMTokenTest( + { redemptionVaultWithMToken, owner, + mTokenLoan, mTBILL, - mFONE, - mFoneToUsdDataFeed, mTokenToUsdDataFeed, - mockedAggregatorMToken, - }; - } + mTokenLoanToUsdDataFeed, + }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); - it('succeeds with remainder-producing rate (mTBILL=1.05, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, + it('should fail: vault has no mTokenLoan and no DAI', async () => { + const { + owner, + redemptionVaultWithMToken, + stableCoins, + mTokenLoan, + mTBILL, + mTokenLoanToUsdDataFeed, + mTokenToUsdDataFeed, + dataFeed, + redemptionVaultLoanSwapper, + } = await loadFixture(defaultDeploy); + + await setLoanLpTest( + { redemptionVault: redemptionVaultWithMToken, owner }, + constants.AddressZero, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await mintToken(mTBILL, owner, 100_000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 100_000, + ); + + await redeemInstantWithMTokenTest( + { redemptionVaultWithMToken, owner, + mTokenLoan, mTBILL, - mFONE, - mockedAggregatorMToken, - } = await setupAaveInnerVault(fixture, key); + mTokenToUsdDataFeed, + mTokenLoanToUsdDataFeed, + }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); - await setRoundData( - { mockedAggregator: mockedAggregatorMToken }, - 1.05, - ); - await mintToken(mFONE, owner, 10000); - await mintToken(mTBILL, redemptionVaultWithMToken, 10000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 10000); + it('redeem 100 mTBILL, when vault has enough DAI and all fees are 0', async () => { + const { + owner, + redemptionVaultWithMToken, + stableCoins, + mTokenLoan, + mTBILL, + mTokenLoanToUsdDataFeed, + mTokenToUsdDataFeed, + dataFeed, + mockedAggregator, + } = await loadFixture(defaultDeploy); + + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); + await setRoundData({ mockedAggregator }, 1); - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits('100'), - 0, - ); + await mintToken(mTBILL, owner, 100_000); + await mintToken( + stableCoins.dai, + redemptionVaultWithMToken, + 1_000_000, + ); + await mintToken(mTokenLoan, redemptionVaultWithMToken, 100_000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 100_000, + ); - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - }); + await setInstantFeeTest( + { vault: redemptionVaultWithMToken, owner }, + 0, + ); - it('succeeds with high mTBILL rate (mTBILL=100, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, + await redeemInstantWithMTokenTest( + { redemptionVaultWithMToken, owner, + mTokenLoan, mTBILL, - mFONE, - mockedAggregatorMToken, - } = await setupAaveInnerVault(fixture, key); + mTokenToUsdDataFeed, + mTokenLoanToUsdDataFeed, + }, + stableCoins.dai, + 100, + ); + }); - await setRoundData( - { mockedAggregator: mockedAggregatorMToken }, - 100, - ); - await mintToken(mFONE, owner, 10000); - await mintToken(mTBILL, redemptionVaultWithMToken, 10000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 10000); + it('redeem 100 mTBILL, when vault has enough DAI (with fees)', async () => { + const { + owner, + redemptionVaultWithMToken, + stableCoins, + mTokenLoan, + mTBILL, + mTokenLoanToUsdDataFeed, + mTokenToUsdDataFeed, + dataFeed, + mockedAggregator, + } = await loadFixture(defaultDeploy); - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits('100'), - 0, - ); + await setRoundData({ mockedAggregator }, 1); - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - }); + await mintToken(mTBILL, owner, 100_000); + await mintToken( + stableCoins.dai, + redemptionVaultWithMToken, + 1_000_000, + ); + await mintToken(mTokenLoan, redemptionVaultWithMToken, 100_000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 100_000, + ); - it('succeeds with near-boundary rate (mTBILL=1.00000003, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, + await redeemInstantWithMTokenTest( + { redemptionVaultWithMToken, owner, + mTokenLoan, mTBILL, - mFONE, - mockedAggregatorMToken, - } = await setupAaveInnerVault(fixture, key); - - await setRoundData( - { mockedAggregator: mockedAggregatorMToken }, - 1.00000003, - ); - await mintToken(mFONE, owner, 100000); - await mintToken(mTBILL, redemptionVaultWithMToken, 100000); - await approveBase18( - owner, - mFONE, - redemptionVaultWithMToken, - 100000, - ); + mTokenToUsdDataFeed, + mTokenLoanToUsdDataFeed, + }, + stableCoins.dai, + 100, + ); + }); - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); + it('redeem 100 mTBILL, vault has no DAI => triggers mTokenLoan redemption', async () => { + const { + owner, + redemptionVaultWithMToken, + stableCoins, + mTokenLoan, + mTBILL, + mTokenLoanToUsdDataFeed, + mTokenToUsdDataFeed, + dataFeed, + redemptionVaultLoanSwapper, + } = await loadFixture(defaultDeploy); - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits('10000'), - 0, - ); + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - }); - }); - } - }); + await mintToken(mTBILL, owner, 100_000); + await mintToken(mTokenLoan, redemptionVaultWithMToken, 100_000); + await mintToken( + stableCoins.dai, + redemptionVaultLoanSwapper, + 1_000_000, + ); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 100_000, + ); - describe('with RedemptionVaultWithMorpho as inner vault', () => { - for (const { key, label } of [ - { key: 'usdc' as const, label: '8-dec' }, - { key: 'usdc6' as const, label: '6-dec' }, - ]) { - describe(`tokenOut ${label} (${key})`, () => { - async function setupMorphoInnerVault( - fixture: Awaited>, - tokenKey: 'usdc' | 'usdc6', - ) { - const { + await redeemInstantWithMTokenTest( + { redemptionVaultWithMToken, - redemptionVaultWithMorpho, - morphoVaultMock, owner, + mTokenLoan, mTBILL, - mFONE, - mFoneToUsdDataFeed, mTokenToUsdDataFeed, - mockedAggregatorMToken, - mockedAggregator, - dataFeed, - stableCoins, - } = fixture; - - const token = stableCoins[tokenKey]; - - await redemptionVaultWithMToken.setRedemptionVault( - redemptionVaultWithMorpho.address, - ); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - token, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - token, - dataFeed.address, - 0, - true, - ); + mTokenLoanToUsdDataFeed, + useMTokenSleeve: true, + additionalLiquidity: async () => { + return ( + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address) + ).div(10 ** (await stableCoins.dai.decimals())); + }, + }, + stableCoins.dai, + 100, + ); + }); - await redemptionVaultWithMorpho.addWaivedFeeAccount( - redemptionVaultWithMToken.address, - ); + it('redeem 100 mTBILL, vault has partial DAI => triggers partial mTokenLoan redemption', async () => { + const { + owner, + redemptionVaultWithMToken, + stableCoins, + mTokenLoan, + mTBILL, + mTokenLoanToUsdDataFeed, + mTokenToUsdDataFeed, + dataFeed, + redemptionVaultLoanSwapper, + } = await loadFixture(defaultDeploy); - if (tokenKey === 'usdc6') { - const { MorphoVaultMock__factory } = await import( - '../../typechain-types' - ); - const morphoUsdc6 = await new MorphoVaultMock__factory( - owner, - ).deploy(token.address); - await token.mint(morphoUsdc6.address, parseUnits('1000000', 6)); - await morphoUsdc6.mint( - redemptionVaultWithMorpho.address, - parseUnits('1000000', 6), - ); - await redemptionVaultWithMorpho.setMorphoVault( - token.address, - morphoUsdc6.address, - ); - } else { - await token.mint(morphoVaultMock.address, parseUnits('1000000')); - await morphoVaultMock.mint( - redemptionVaultWithMorpho.address, - parseUnits('1000000'), - ); - } + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); - await setInstantFeeTest( - { vault: redemptionVaultWithMToken, owner }, - 0, - ); + await mintToken(mTBILL, owner, 100_000); + await mintToken(mTokenLoan, redemptionVaultWithMToken, 100_000); + await mintToken(stableCoins.dai, redemptionVaultWithMToken, 10); + await mintToken( + stableCoins.dai, + redemptionVaultLoanSwapper, + 1_000_000, + ); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 100_000, + ); - return { - token, + await redeemInstantWithMTokenTest( + { redemptionVaultWithMToken, owner, + mTokenLoan, mTBILL, - mFONE, - mFoneToUsdDataFeed, mTokenToUsdDataFeed, - mockedAggregatorMToken, - }; - } + mTokenLoanToUsdDataFeed, + useMTokenSleeve: true, + additionalLiquidity: async () => { + return ( + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address) + ).div(10 ** (await stableCoins.dai.decimals())); + }, + }, + stableCoins.dai, + 100, + ); + }); - it('succeeds with remainder-producing rate (mTBILL=1.05, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mockedAggregatorMToken, - } = await setupMorphoInnerVault(fixture, key); + it('redeem 100 mTBILL with divergent rates (mTBILL=$5, mTokenLoan=$2) => triggers mTokenLoan redemption', async () => { + const { + owner, + redemptionVaultWithMToken, + stableCoins, + mTokenLoan, + mTBILL, + mTokenLoanToUsdDataFeed, + mTokenToUsdDataFeed, + dataFeed, + redemptionVaultLoanSwapper, + mockedAggregatorMTokenLoan, + } = await loadFixture(defaultDeploy); - await setRoundData( - { mockedAggregator: mockedAggregatorMToken }, - 1.05, - ); - await mintToken(mFONE, owner, 10000); - await mintToken(mTBILL, redemptionVaultWithMToken, 10000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 10000); + await setWaivedFeeAccountTest( + { vault: redemptionVaultWithMToken, owner }, + redemptionVaultLoanSwapper.address, + true, + ); - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits('100'), - 0, - ); + await setRoundData( + { mockedAggregator: mockedAggregatorMTokenLoan }, + 2, + ); - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - }); + await mintToken(mTBILL, owner, 100_000); + await mintToken(mTokenLoan, redemptionVaultWithMToken, 100_000); + await mintToken( + stableCoins.dai, + redemptionVaultLoanSwapper, + 1_000_000, + ); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 100_000, + ); - it('succeeds with high mTBILL rate (mTBILL=100, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, + await redeemInstantWithMTokenTest( + { redemptionVaultWithMToken, owner, + mTokenLoan, mTBILL, - mFONE, - mockedAggregatorMToken, - } = await setupMorphoInnerVault(fixture, key); + mTokenToUsdDataFeed, + mTokenLoanToUsdDataFeed, + useMTokenSleeve: true, + additionalLiquidity: async () => { + return ( + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address) + ) + .div(10 ** (await stableCoins.dai.decimals())) + .mul(2); + }, + }, + stableCoins.dai, + 100, + ); + }); - await setRoundData( - { mockedAggregator: mockedAggregatorMToken }, - 100, - ); - await mintToken(mFONE, owner, 10000); - await mintToken(mTBILL, redemptionVaultWithMToken, 10000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 10000); + it('redeem with waived fee', async () => { + const { + owner, + redemptionVaultWithMToken, + stableCoins, + mTokenLoan, + mTBILL, + mTokenLoanToUsdDataFeed, + mTokenToUsdDataFeed, + dataFeed, + mockedAggregator, + } = await loadFixture(defaultDeploy); - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setWaivedFeeAccountTest( + { vault: redemptionVaultWithMToken, owner }, + owner.address, + true, + ); - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits('100'), - 0, - ); + await setRoundData({ mockedAggregator }, 1); - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - }); + await mintToken(mTBILL, owner, 100_000); + await mintToken( + stableCoins.dai, + redemptionVaultWithMToken, + 1_000_000, + ); + await mintToken(mTokenLoan, redemptionVaultWithMToken, 100_000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 100_000, + ); - it('succeeds with near-boundary rate (mTBILL=1.00000003, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, + await redeemInstantWithMTokenTest( + { redemptionVaultWithMToken, owner, + mTokenLoan, mTBILL, - mFONE, - mockedAggregatorMToken, - } = await setupMorphoInnerVault(fixture, key); - - await setRoundData( - { mockedAggregator: mockedAggregatorMToken }, - 1.00000003, - ); - await mintToken(mFONE, owner, 100000); - await mintToken(mTBILL, redemptionVaultWithMToken, 100000); - await approveBase18( - owner, - mFONE, - redemptionVaultWithMToken, - 100000, - ); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); + mTokenToUsdDataFeed, + mTokenLoanToUsdDataFeed, + waivedFee: true, + }, + stableCoins.dai, + 100, + ); + }); + }); - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits('10000'), - 0, - ); + describe('ceiling division math correctness', () => { + const TOKEN_DECIMALS: Record = { + usdc6: 6, + usdc: 8, + dai: 9, + usdt: 18, + }; - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - }); - }); - } - }); + /** + * Mirrors the Solidity math in RedemptionVaultWithMToken._redeemInstant: + * amountTokenOutWithoutFee = truncate( + * (amountMTokenWithoutFee * mTokenRate / tokenOutRate), tokenDecimals + * ) + * Returns the expected user output in NATIVE decimals. + */ + function computeExpectedTokenOut( + amountMTokenIn: BigNumber, + mTokenRate: BigNumber, + tokenOutRate: BigNumber, + tokenDecimals: number, + feePercent: number, + isWaived: boolean, + ): BigNumber { + const fee = isWaived + ? BigNumber.from(0) + : amountMTokenIn.mul(feePercent).div(10000); + const amountWithoutFee = amountMTokenIn.sub(fee); + const base18Out = amountWithoutFee.mul(mTokenRate).div(tokenOutRate); + const scale = BigNumber.from(10).pow(18 - tokenDecimals); + const truncated = base18Out.div(scale); + return truncated; + } - describe('with RedemptionVaultWithUSTB as inner vault', () => { - describe('tokenOut 8-dec (usdc — USTB only supports its configured USDC)', () => { - async function setupUstbInnerVault( - fixture: Awaited>, - ) { + /** + * Lean helper: configures both outer + inner vault for a given tokenOut, + * sets rates, mints tokens, and returns everything needed to call redeemInstant. + * The outer vault has ZERO tokenOut to force the inner-vault redemption path. + */ + async function setupCeilDivTest(opts: { + fixture: Awaited>; + tokenKey: 'usdc6' | 'usdc' | 'dai' | 'usdt'; + mTbillRate: number; + isStable: boolean; + tokenOutRate?: number; + redeemAmount?: number; + }) { const { redemptionVaultWithMToken, - redemptionVaultWithUSTB, - ustbToken, - ustbRedemption, + redemptionVaultLoanSwapper, owner, + mTokenLoan, mTBILL, - mFONE, - mFoneToUsdDataFeed, mTokenToUsdDataFeed, + mTokenLoanToUsdDataFeed, mockedAggregatorMToken, + mockedAggregatorMTokenLoan, mockedAggregator, dataFeed, stableCoins, - } = fixture; - - const token = stableCoins.usdc; + } = opts.fixture; - await redemptionVaultWithMToken.setRedemptionVault( - redemptionVaultWithUSTB.address, - ); + const token = stableCoins[opts.tokenKey]; await addPaymentTokenTest( { vault: redemptionVaultWithMToken, owner }, token, dataFeed.address, 0, - true, + opts.isStable, ); await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, + { vault: redemptionVaultLoanSwapper, owner }, token, dataFeed.address, 0, - true, - ); - - await redemptionVaultWithUSTB.addWaivedFeeAccount( - redemptionVaultWithMToken.address, + opts.isStable, ); - await ustbToken.mint( - redemptionVaultWithUSTB.address, - parseUnits('1000000', 6), + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + opts.mTbillRate, ); - await token.mint(ustbRedemption.address, parseUnits('1000000')); + if (opts.tokenOutRate !== undefined) { + await setRoundData({ mockedAggregator }, opts.tokenOutRate); + } await setInstantFeeTest( { vault: redemptionVaultWithMToken, owner }, 0, ); + const amount = opts.redeemAmount ?? 100; + + await mintToken(mTBILL, owner, amount * 100); + await mintToken(mTokenLoan, redemptionVaultWithMToken, amount * 100); + await mintToken(token, redemptionVaultLoanSwapper, amount * 10000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + amount * 100, + ); + + const mFoneRate = await mTokenToUsdDataFeed.getDataInBase18(); + const tokenOutRate = opts.isStable + ? parseUnits('1') + : await dataFeed.getDataInBase18(); + return { token, + amount, redemptionVaultWithMToken, + redemptionVaultLoanSwapper, owner, + mTokenLoan, mTBILL, - mFONE, - mFoneToUsdDataFeed, mTokenToUsdDataFeed, - mockedAggregatorMToken, + mTokenLoanToUsdDataFeed, + mockedAggregatorMTokenLoan, + mockedAggregator, + mFoneRate, + tokenOutRate, }; } - it('succeeds with remainder-producing rate (mTBILL=1.05, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mockedAggregatorMToken, - } = await setupUstbInnerVault(fixture); + describe('with base RedemptionVault as inner vault', () => { + const tokenVariants: Array<{ + key: 'usdc6' | 'usdc' | 'dai' | 'usdt'; + label: string; + }> = [ + { key: 'usdc6', label: '6-dec' }, + { key: 'usdc', label: '8-dec' }, + { key: 'dai', label: '9-dec' }, + { key: 'usdt', label: '18-dec' }, + ]; + + for (const { key, label } of tokenVariants) { + describe(`tokenOut ${label} (${key})`, () => { + it('succeeds with exact division (mTokenLoan=5, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + amount, + redemptionVaultWithMToken, + owner, + mTokenLoan, + mFoneRate, + tokenOutRate, + } = await setupCeilDivTest({ + fixture, + tokenKey: key, + mTbillRate: 5, + isStable: true, + }); + + const mTbillBefore = await mTokenLoan.balanceOf( + redemptionVaultWithMToken.address, + ); + const userTokenBefore = await token.balanceOf(owner.address); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits(amount.toString()), + 0, + ); + + expect( + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + + const expected = computeExpectedTokenOut( + parseUnits(amount.toString()), + mFoneRate, + tokenOutRate, + TOKEN_DECIMALS[key], + 0, + true, + ); + const userTokenAfter = await token.balanceOf(owner.address); + expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); + }); + + it('succeeds with remainder-producing rate (mTokenLoan=1.05, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + amount, + redemptionVaultWithMToken, + owner, + mTokenLoan, + mFoneRate, + tokenOutRate, + } = await setupCeilDivTest({ + fixture, + tokenKey: key, + mTbillRate: 1.05, + isStable: true, + }); + + const mTbillBefore = await mTokenLoan.balanceOf( + redemptionVaultWithMToken.address, + ); + const userTokenBefore = await token.balanceOf(owner.address); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits(amount.toString()), + 0, + ); + + expect( + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + + const expected = computeExpectedTokenOut( + parseUnits(amount.toString()), + mFoneRate, + tokenOutRate, + TOKEN_DECIMALS[key], + 0, + true, + ); + const userTokenAfter = await token.balanceOf(owner.address); + expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); + }); + + it('succeeds with high mTokenLoan rate (mTokenLoan=100, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + amount, + redemptionVaultWithMToken, + owner, + mTokenLoan, + mFoneRate, + tokenOutRate, + } = await setupCeilDivTest({ + fixture, + tokenKey: key, + mTbillRate: 100, + isStable: true, + }); + + const mTbillBefore = await mTokenLoan.balanceOf( + redemptionVaultWithMToken.address, + ); + const userTokenBefore = await token.balanceOf(owner.address); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits(amount.toString()), + 0, + ); + + expect( + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + + const expected = computeExpectedTokenOut( + parseUnits(amount.toString()), + mFoneRate, + tokenOutRate, + TOKEN_DECIMALS[key], + 0, + true, + ); + const userTokenAfter = await token.balanceOf(owner.address); + expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); + }); + + it('succeeds with low mTokenLoan rate (mTokenLoan=0.5, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + amount, + redemptionVaultWithMToken, + owner, + mTokenLoan, + mFoneRate, + tokenOutRate, + } = await setupCeilDivTest({ + fixture, + tokenKey: key, + mTbillRate: 0.5, + isStable: true, + }); + + const mTbillBefore = await mTokenLoan.balanceOf( + redemptionVaultWithMToken.address, + ); + const userTokenBefore = await token.balanceOf(owner.address); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits(amount.toString()), + 0, + ); + + expect( + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + + const expected = computeExpectedTokenOut( + parseUnits(amount.toString()), + mFoneRate, + tokenOutRate, + TOKEN_DECIMALS[key], + 0, + true, + ); + const userTokenAfter = await token.balanceOf(owner.address); + expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); + }); + + it('succeeds with near-boundary rate (mTokenLoan=1.00000003, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + redemptionVaultWithMToken, + owner, + mTokenLoan, + mFoneRate, + tokenOutRate, + } = await setupCeilDivTest({ + fixture, + tokenKey: key, + mTbillRate: 1.00000003, + isStable: true, + redeemAmount: 10000, + }); + + const mTbillBefore = await mTokenLoan.balanceOf( + redemptionVaultWithMToken.address, + ); + const userTokenBefore = await token.balanceOf(owner.address); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits('10000'), + 0, + ); + + expect( + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + + const expected = computeExpectedTokenOut( + parseUnits('10000'), + mFoneRate, + tokenOutRate, + TOKEN_DECIMALS[key], + 0, + true, + ); + const userTokenAfter = await token.balanceOf(owner.address); + expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); + }); + + it('succeeds with non-stable tokenOut (mTokenLoan=1.05, tokenOut=1.03)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + amount, + redemptionVaultWithMToken, + owner, + mTokenLoan, + mFoneRate, + tokenOutRate, + } = await setupCeilDivTest({ + fixture, + tokenKey: key, + mTbillRate: 1.05, + isStable: false, + tokenOutRate: 1.03, + }); + + const mTbillBefore = await mTokenLoan.balanceOf( + redemptionVaultWithMToken.address, + ); + const userTokenBefore = await token.balanceOf(owner.address); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits(amount.toString()), + 0, + ); + + expect( + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + + const expected = computeExpectedTokenOut( + parseUnits(amount.toString()), + mFoneRate, + tokenOutRate, + TOKEN_DECIMALS[key], + 0, + true, + ); + const userTokenAfter = await token.balanceOf(owner.address); + expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); + }); + + it('succeeds with small redeem (1 mTBILL, mTokenLoan=1.05, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + redemptionVaultWithMToken, + owner, + mTokenLoan, + mFoneRate, + tokenOutRate, + } = await setupCeilDivTest({ + fixture, + tokenKey: key, + mTbillRate: 1.05, + isStable: true, + redeemAmount: 1, + }); + + const mTbillBefore = await mTokenLoan.balanceOf( + redemptionVaultWithMToken.address, + ); + const userTokenBefore = await token.balanceOf(owner.address); + + await setMinAmountTest( + { vault: redemptionVaultWithMToken, owner }, + 0, + ); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits('1'), + 0, + ); + + expect( + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + + const expected = computeExpectedTokenOut( + parseUnits('1'), + mFoneRate, + tokenOutRate, + TOKEN_DECIMALS[key], + 0, + true, + ); + const userTokenAfter = await token.balanceOf(owner.address); + expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); + }); + + it('succeeds with large redeem near daily limit (mTokenLoan=1.05, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + redemptionVaultWithMToken, + owner, + mTokenLoan, + mFoneRate, + tokenOutRate, + } = await setupCeilDivTest({ + fixture, + tokenKey: key, + mTbillRate: 1.05, + isStable: true, + redeemAmount: 50000, + }); + + const mTbillBefore = await mTokenLoan.balanceOf( + redemptionVaultWithMToken.address, + ); + const userTokenBefore = await token.balanceOf(owner.address); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits('50000'), + 0, + ); + + expect( + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + + const expected = computeExpectedTokenOut( + parseUnits('50000'), + mFoneRate, + tokenOutRate, + TOKEN_DECIMALS[key], + 0, + true, + ); + const userTokenAfter = await token.balanceOf(owner.address); + expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); + }); + }); + } + }); - await setRoundData( - { mockedAggregator: mockedAggregatorMToken }, - 1.05, - ); - await mintToken(mFONE, owner, 10000); - await mintToken(mTBILL, redemptionVaultWithMToken, 10000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 10000); + describe('with RedemptionVaultWithAave as inner vault', () => { + for (const { key, label } of [ + { key: 'usdc' as const, label: '8-dec' }, + { key: 'usdc6' as const, label: '6-dec' }, + ]) { + describe(`tokenOut ${label} (${key})`, () => { + async function setupAaveInnerVault( + fixture: Awaited>, + tokenKey: 'usdc' | 'usdc6', + ) { + const { + redemptionVaultWithMToken, + redemptionVaultWithAave, + aavePoolMock, + aUSDC, + owner, + mTBILL, + mTokenToUsdDataFeed, + mTokenLoanToUsdDataFeed, + mockedAggregatorMToken, + dataFeed, + stableCoins, + } = fixture; + + const token = stableCoins[tokenKey]; + + await redemptionVaultWithMToken.setRedemptionVault( + redemptionVaultWithAave.address, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + token, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultWithAave, owner }, + token, + dataFeed.address, + 0, + true, + ); + + await redemptionVaultWithAave.setWaivedFeeAccount( + redemptionVaultWithMToken.address, + true, + ); + + if (tokenKey === 'usdc6') { + const { ERC20Mock__factory } = await import( + '../../typechain-types' + ); + const aUsdc6 = await new ERC20Mock__factory(owner).deploy(6); + await aavePoolMock.setReserveAToken( + token.address, + aUsdc6.address, + ); + await token.mint( + aavePoolMock.address, + parseUnits('1000000', 6), + ); + await aUsdc6.mint( + redemptionVaultWithAave.address, + parseUnits('1000000', 6), + ); + await redemptionVaultWithAave.setAavePool( + token.address, + aavePoolMock.address, + ); + } else { + await token.mint(aavePoolMock.address, parseUnits('1000000')); + await aUSDC.mint( + redemptionVaultWithAave.address, + parseUnits('1000000'), + ); + await redemptionVaultWithAave.setAavePool( + token.address, + aavePoolMock.address, + ); + } + + await setInstantFeeTest( + { vault: redemptionVaultWithMToken, owner }, + 0, + ); + + return { + token, + redemptionVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + mTokenLoanToUsdDataFeed, + mockedAggregatorMToken, + }; + } + + it('succeeds with remainder-producing rate (mTokenLoan=1.05, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + redemptionVaultWithMToken, + owner, + mTBILL, + mockedAggregatorMToken, + } = await setupAaveInnerVault(fixture, key); + + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 1.05, + ); + await mintToken(mTBILL, owner, 10000); + await mintToken(mTBILL, redemptionVaultWithMToken, 10000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 10000, + ); + + const mTbillBefore = await mTBILL.balanceOf( + redemptionVaultWithMToken.address, + ); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits('100'), + 0, + ); + + expect( + await mTBILL.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + }); + + it('succeeds with high mTokenLoan rate (mTokenLoan=100, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + redemptionVaultWithMToken, + owner, + mTBILL, + mockedAggregatorMToken, + } = await setupAaveInnerVault(fixture, key); + + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 100, + ); + await mintToken(mTBILL, owner, 10000); + await mintToken(mTBILL, redemptionVaultWithMToken, 10000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 10000, + ); + + const mTbillBefore = await mTBILL.balanceOf( + redemptionVaultWithMToken.address, + ); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits('100'), + 0, + ); + + expect( + await mTBILL.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + }); + + it('succeeds with near-boundary rate (mTokenLoan=1.00000003, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + redemptionVaultWithMToken, + owner, + mTBILL, + mockedAggregatorMToken, + } = await setupAaveInnerVault(fixture, key); + + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 1.00000003, + ); + await mintToken(mTBILL, owner, 100000); + await mintToken(mTBILL, redemptionVaultWithMToken, 100000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 100000, + ); + + const mTbillBefore = await mTBILL.balanceOf( + redemptionVaultWithMToken.address, + ); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits('10000'), + 0, + ); + + expect( + await mTBILL.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + }); + }); + } + }); - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); + describe('with RedemptionVaultWithMorpho as inner vault', () => { + for (const { key, label } of [ + { key: 'usdc' as const, label: '8-dec' }, + { key: 'usdc6' as const, label: '6-dec' }, + ]) { + describe(`tokenOut ${label} (${key})`, () => { + async function setupMorphoInnerVault( + fixture: Awaited>, + tokenKey: 'usdc' | 'usdc6', + ) { + const { + redemptionVaultWithMToken, + redemptionVaultWithMorpho, + morphoVaultMock, + owner, + mTBILL, + mTokenToUsdDataFeed, + mTokenLoanToUsdDataFeed, + mockedAggregatorMToken, + dataFeed, + stableCoins, + } = fixture; + + const token = stableCoins[tokenKey]; + + await redemptionVaultWithMToken.setRedemptionVault( + redemptionVaultWithMorpho.address, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + token, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultWithMorpho, owner }, + token, + dataFeed.address, + 0, + true, + ); + + await redemptionVaultWithMorpho.setWaivedFeeAccount( + redemptionVaultWithMToken.address, + true, + ); + + if (tokenKey === 'usdc6') { + const { MorphoVaultMock__factory } = await import( + '../../typechain-types' + ); + const morphoUsdc6 = await new MorphoVaultMock__factory( + owner, + ).deploy(token.address); + await token.mint( + morphoUsdc6.address, + parseUnits('1000000', 6), + ); + await morphoUsdc6.mint( + redemptionVaultWithMorpho.address, + parseUnits('1000000', 6), + ); + await redemptionVaultWithMorpho.setMorphoVault( + token.address, + morphoUsdc6.address, + ); + } else { + await token.mint( + morphoVaultMock.address, + parseUnits('1000000'), + ); + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('1000000'), + ); + await redemptionVaultWithMorpho.setMorphoVault( + token.address, + morphoVaultMock.address, + ); + } + + await setInstantFeeTest( + { vault: redemptionVaultWithMToken, owner }, + 0, + ); + + return { + token, + redemptionVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + mTokenLoanToUsdDataFeed, + mockedAggregatorMToken, + }; + } + + it('succeeds with remainder-producing rate (mTokenLoan=1.05, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + redemptionVaultWithMToken, + owner, + mTBILL, + mockedAggregatorMToken, + } = await setupMorphoInnerVault(fixture, key); + + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 1.05, + ); + await mintToken(mTBILL, owner, 10000); + await mintToken(mTBILL, redemptionVaultWithMToken, 10000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 10000, + ); + + const mTbillBefore = await mTBILL.balanceOf( + redemptionVaultWithMToken.address, + ); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits('100'), + 0, + ); + + expect( + await mTBILL.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + }); + + it('succeeds with high mTokenLoan rate (mTokenLoan=100, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + redemptionVaultWithMToken, + owner, + mTBILL, + mockedAggregatorMToken, + } = await setupMorphoInnerVault(fixture, key); + + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 100, + ); + await mintToken(mTBILL, owner, 10000); + await mintToken(mTBILL, redemptionVaultWithMToken, 10000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 10000, + ); + + const mTbillBefore = await mTBILL.balanceOf( + redemptionVaultWithMToken.address, + ); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits('100'), + 0, + ); + + expect( + await mTBILL.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + }); + + it('succeeds with near-boundary rate (mTokenLoan=1.00000003, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + redemptionVaultWithMToken, + owner, + mTBILL, + mockedAggregatorMToken, + } = await setupMorphoInnerVault(fixture, key); + + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 1.00000003, + ); + await mintToken(mTBILL, owner, 100000); + await mintToken(mTBILL, redemptionVaultWithMToken, 100000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 100000, + ); + + const mTbillBefore = await mTBILL.balanceOf( + redemptionVaultWithMToken.address, + ); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits('10000'), + 0, + ); + + expect( + await mTBILL.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + }); + }); + } + }); - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits('100'), - 0, - ); + describe('with RedemptionVaultWithUSTB as inner vault', () => { + describe('tokenOut 8-dec (usdc — USTB only supports its configured USDC)', () => { + async function setupUstbInnerVault( + fixture: Awaited>, + ) { + const { + redemptionVaultWithMToken, + redemptionVaultWithUSTB, + ustbToken, + ustbRedemption, + owner, + mTBILL, + mTokenToUsdDataFeed, + mTokenLoanToUsdDataFeed, + mockedAggregatorMToken, + dataFeed, + stableCoins, + } = fixture; + + const token = stableCoins.usdc; + + await redemptionVaultWithMToken.setRedemptionVault( + redemptionVaultWithUSTB.address, + ); - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - }); + await addPaymentTokenTest( + { vault: redemptionVaultWithMToken, owner }, + token, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultWithUSTB, owner }, + token, + dataFeed.address, + 0, + true, + ); - it('succeeds with high mTBILL rate (mTBILL=100, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mockedAggregatorMToken, - } = await setupUstbInnerVault(fixture); + await redemptionVaultWithUSTB.setWaivedFeeAccount( + redemptionVaultWithMToken.address, + true, + ); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 100); - await mintToken(mFONE, owner, 10000); - await mintToken(mTBILL, redemptionVaultWithMToken, 10000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 10000); + await ustbToken.mint( + redemptionVaultWithUSTB.address, + parseUnits('1000000', 6), + ); + await token.mint(ustbRedemption.address, parseUnits('1000000')); - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); + await setInstantFeeTest( + { vault: redemptionVaultWithMToken, owner }, + 0, + ); - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits('100'), - 0, - ); + return { + token, + redemptionVaultWithMToken, + owner, + mTBILL, + mTokenToUsdDataFeed, + mTokenLoanToUsdDataFeed, + mockedAggregatorMToken, + }; + } - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - }); + it('succeeds with remainder-producing rate (mTokenLoan=1.05, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + redemptionVaultWithMToken, + owner, + mTBILL, + mockedAggregatorMToken, + } = await setupUstbInnerVault(fixture); - it('succeeds with near-boundary rate (mTBILL=1.00000003, stable)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - redemptionVaultWithMToken, - owner, - mTBILL, - mFONE, - mockedAggregatorMToken, - } = await setupUstbInnerVault(fixture); + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 1.05, + ); + await mintToken(mTBILL, owner, 10000); + await mintToken(mTBILL, redemptionVaultWithMToken, 10000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 10000, + ); - await setRoundData( - { mockedAggregator: mockedAggregatorMToken }, - 1.00000003, - ); - await mintToken(mFONE, owner, 100000); - await mintToken(mTBILL, redemptionVaultWithMToken, 100000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 100000); + const mTbillBefore = await mTBILL.balanceOf( + redemptionVaultWithMToken.address, + ); - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits('100'), + 0, + ); + + expect( + await mTBILL.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + }); - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits('10000'), - 0, - ); + it('succeeds with high mTokenLoan rate (mTokenLoan=100, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + redemptionVaultWithMToken, + owner, + mTBILL, + mockedAggregatorMToken, + } = await setupUstbInnerVault(fixture); - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 100, + ); + await mintToken(mTBILL, owner, 10000); + await mintToken(mTBILL, redemptionVaultWithMToken, 10000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 10000, + ); + + const mTbillBefore = await mTBILL.balanceOf( + redemptionVaultWithMToken.address, + ); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits('100'), + 0, + ); + + expect( + await mTBILL.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + }); + + it('succeeds with near-boundary rate (mTokenLoan=1.00000003, stable)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + redemptionVaultWithMToken, + owner, + mTBILL, + mockedAggregatorMToken, + } = await setupUstbInnerVault(fixture); + + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 1.00000003, + ); + await mintToken(mTBILL, owner, 100000); + await mintToken(mTBILL, redemptionVaultWithMToken, 100000); + await approveBase18( + owner, + mTBILL, + redemptionVaultWithMToken, + 100000, + ); + + const mTbillBefore = await mTBILL.balanceOf( + redemptionVaultWithMToken.address, + ); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits('10000'), + 0, + ); + + expect( + await mTBILL.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + }); + }); }); - }); - }); - describe('with RedemptionVaultWithSwapper as inner vault - direct path', () => { - async function setupSwapperDirectPath( - fixture: Awaited>, - tokenKey: 'usdc6' | 'usdc' | 'dai' | 'usdt', - ) { - const { - redemptionVaultWithMToken, - redemptionVaultWithSwapper, - owner, - mBASIS, - mFONE, - mFoneToUsdDataFeed, - mBasisToUsdDataFeed, - mockedAggregatorMBasis, - mockedAggregator, - dataFeed, - stableCoins, - } = fixture; - - const token = stableCoins[tokenKey]; - - await redemptionVaultWithMToken.setRedemptionVault( - redemptionVaultWithSwapper.address, - ); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - token, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - token, - dataFeed.address, - 0, - true, - ); - - await redemptionVaultWithSwapper.addWaivedFeeAccount( - redemptionVaultWithMToken.address, - ); - - await setInstantFeeTest({ vault: redemptionVaultWithMToken, owner }, 0); - - await mintToken(token, redemptionVaultWithSwapper, 1_000_000); - - return { - token, - redemptionVaultWithMToken, - redemptionVaultWithSwapper, - owner, - mBASIS, - mFONE, - mFoneToUsdDataFeed, - mBasisToUsdDataFeed, - mockedAggregatorMBasis, - mockedAggregator, - }; - } - - for (const { key, label } of [ - { key: 'usdc6' as const, label: '6-dec' }, - { key: 'usdc' as const, label: '8-dec' }, - { key: 'dai' as const, label: '9-dec' }, - { key: 'usdt' as const, label: '18-dec' }, - ]) { - describe(`tokenOut ${label} (${key})`, () => { - it('succeeds with exact division (mBasis=5, stable)', async () => { + describe('with outer vault fee enabled', () => { + it('succeeds with 1% fee, usdc6 (6-dec), mTokenLoan=1.05, stable', async () => { const fixture = await loadFixture(defaultDeploy); const { token, + amount, redemptionVaultWithMToken, owner, - mBASIS, - mFONE, - mockedAggregatorMBasis, - } = await setupSwapperDirectPath(fixture, key); + mTokenLoan, + } = await setupCeilDivTest({ + fixture, + tokenKey: 'usdc6', + mTbillRate: 1.05, + isStable: true, + }); - await setRoundData({ mockedAggregator: mockedAggregatorMBasis }, 5); - await mintToken(mFONE, owner, 10000); - await mintToken(mBASIS, redemptionVaultWithMToken, 10000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 10000); + await setInstantFeeTest( + { vault: redemptionVaultWithMToken, owner }, + 100, + ); - const mBasisBefore = await mBASIS.balanceOf( + const mTbillBefore = await mTokenLoan.balanceOf( redemptionVaultWithMToken.address, ); + const userTokenBefore = await token.balanceOf(owner.address); await redemptionVaultWithMToken .connect(owner) ['redeemInstant(address,uint256,uint256)']( token.address, - parseUnits('100'), + parseUnits(amount.toString()), 0, ); expect( - await mBASIS.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mBasisBefore); + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + expect(await token.balanceOf(owner.address)).to.be.gt( + userTokenBefore, + ); }); - it('succeeds with remainder-producing rate (mBasis=1.05, stable)', async () => { + it('succeeds with 1% fee, usdc (8-dec), mTokenLoan=1.05, stable', async () => { const fixture = await loadFixture(defaultDeploy); const { token, + amount, redemptionVaultWithMToken, owner, - mBASIS, - mFONE, - mockedAggregatorMBasis, - } = await setupSwapperDirectPath(fixture, key); + mTokenLoan, + } = await setupCeilDivTest({ + fixture, + tokenKey: 'usdc', + mTbillRate: 1.05, + isStable: true, + }); - await setRoundData( - { mockedAggregator: mockedAggregatorMBasis }, - 1.05, + await setInstantFeeTest( + { vault: redemptionVaultWithMToken, owner }, + 100, ); - await mintToken(mFONE, owner, 10000); - await mintToken(mBASIS, redemptionVaultWithMToken, 10000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 10000); - const mBasisBefore = await mBASIS.balanceOf( + const mTbillBefore = await mTokenLoan.balanceOf( redemptionVaultWithMToken.address, ); const userTokenBefore = await token.balanceOf(owner.address); @@ -4128,79 +2466,80 @@ describe('RedemptionVaultWithMToken', function () { .connect(owner) ['redeemInstant(address,uint256,uint256)']( token.address, - parseUnits('100'), + parseUnits(amount.toString()), 0, ); expect( - await mBASIS.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mBasisBefore); + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); expect(await token.balanceOf(owner.address)).to.be.gt( userTokenBefore, ); }); - it('succeeds with high rate (mBasis=100, stable)', async () => { + it('succeeds with 1% fee, usdt (18-dec), mTokenLoan=1.05, stable', async () => { const fixture = await loadFixture(defaultDeploy); const { token, + amount, redemptionVaultWithMToken, owner, - mBASIS, - mFONE, - mockedAggregatorMBasis, - } = await setupSwapperDirectPath(fixture, key); + mTokenLoan, + } = await setupCeilDivTest({ + fixture, + tokenKey: 'usdt', + mTbillRate: 1.05, + isStable: true, + }); - await setRoundData( - { mockedAggregator: mockedAggregatorMBasis }, + await setInstantFeeTest( + { vault: redemptionVaultWithMToken, owner }, 100, ); - await mintToken(mFONE, owner, 10000); - await mintToken(mBASIS, redemptionVaultWithMToken, 10000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 10000); - const mBasisBefore = await mBASIS.balanceOf( + const mTbillBefore = await mTokenLoan.balanceOf( redemptionVaultWithMToken.address, ); + const userTokenBefore = await token.balanceOf(owner.address); await redemptionVaultWithMToken .connect(owner) ['redeemInstant(address,uint256,uint256)']( token.address, - parseUnits('100'), + parseUnits(amount.toString()), 0, ); expect( - await mBASIS.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mBasisBefore); + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + expect(await token.balanceOf(owner.address)).to.be.gt( + userTokenBefore, + ); }); - it('succeeds with near-boundary rate (mBasis=1.00000003, stable)', async () => { + it('succeeds with 1% fee, usdc6 (6-dec), mTokenLoan=100, stable', async () => { const fixture = await loadFixture(defaultDeploy); const { token, + amount, redemptionVaultWithMToken, owner, - mBASIS, - mFONE, - mockedAggregatorMBasis, - } = await setupSwapperDirectPath(fixture, key); + mTokenLoan, + } = await setupCeilDivTest({ + fixture, + tokenKey: 'usdc6', + mTbillRate: 100, + isStable: true, + }); - await setRoundData( - { mockedAggregator: mockedAggregatorMBasis }, - 1.00000003, - ); - await mintToken(mFONE, owner, 100000); - await mintToken(mBASIS, redemptionVaultWithMToken, 100000); - await approveBase18( - owner, - mFONE, - redemptionVaultWithMToken, - 100000, + await setInstantFeeTest( + { vault: redemptionVaultWithMToken, owner }, + 100, ); - const mBasisBefore = await mBASIS.balanceOf( + const mTbillBefore = await mTokenLoan.balanceOf( redemptionVaultWithMToken.address, ); @@ -4208,60 +2547,74 @@ describe('RedemptionVaultWithMToken', function () { .connect(owner) ['redeemInstant(address,uint256,uint256)']( token.address, - parseUnits('10000'), + parseUnits(amount.toString()), 0, ); expect( - await mBASIS.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mBasisBefore); + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); }); - it('succeeds with non-stable tokenOut (mBasis=1.05, tokenOut=1.03)', async () => { + it('succeeds with 1% fee, usdc (8-dec), non-stable mTokenLoan=1.05 tokenOut=1.03', async () => { const fixture = await loadFixture(defaultDeploy); const { token, + amount, redemptionVaultWithMToken, owner, - mBASIS, - mFONE, - mockedAggregatorMBasis, - mockedAggregator, - } = await setupSwapperDirectPath(fixture, key); + mTokenLoan, + } = await setupCeilDivTest({ + fixture, + tokenKey: 'usdc', + mTbillRate: 1.05, + isStable: false, + tokenOutRate: 1.03, + }); - await removePaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - token, - ); - await addPaymentTokenTest( + await setInstantFeeTest( { vault: redemptionVaultWithMToken, owner }, - token, - fixture.dataFeed.address, - 0, - false, + 100, ); - await removePaymentTokenTest( - { vault: fixture.redemptionVaultWithSwapper, owner }, - token, + + const mTbillBefore = await mTokenLoan.balanceOf( + redemptionVaultWithMToken.address, ); - await addPaymentTokenTest( - { vault: fixture.redemptionVaultWithSwapper, owner }, - token, - fixture.dataFeed.address, - 0, - false, + const userTokenBefore = await token.balanceOf(owner.address); + + await redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits(amount.toString()), + 0, + ); + + expect( + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + expect(await token.balanceOf(owner.address)).to.be.gt( + userTokenBefore, ); + }); - await setRoundData( - { mockedAggregator: mockedAggregatorMBasis }, - 1.05, + it('succeeds with 1% fee, usdc6 (6-dec), near-boundary mTokenLoan=1.00000003', async () => { + const fixture = await loadFixture(defaultDeploy); + const { token, redemptionVaultWithMToken, owner, mTokenLoan } = + await setupCeilDivTest({ + fixture, + tokenKey: 'usdc6', + mTbillRate: 1.00000003, + isStable: true, + redeemAmount: 10000, + }); + + await setInstantFeeTest( + { vault: redemptionVaultWithMToken, owner }, + 100, ); - await setRoundData({ mockedAggregator }, 1.03); - await mintToken(mFONE, owner, 10000); - await mintToken(mBASIS, redemptionVaultWithMToken, 10000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 10000); - const mBasisBefore = await mBASIS.balanceOf( + const mTbillBefore = await mTokenLoan.balanceOf( redemptionVaultWithMToken.address, ); @@ -4269,794 +2622,319 @@ describe('RedemptionVaultWithMToken', function () { .connect(owner) ['redeemInstant(address,uint256,uint256)']( token.address, - parseUnits('100'), + parseUnits('10000'), 0, ); expect( - await mBASIS.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mBasisBefore); + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); }); }); - } - }); - describe('with RedemptionVaultWithSwapper as inner vault - swap path', () => { - async function setupSwapperSwapPath( - fixture: Awaited>, - tokenKey: 'usdc6' | 'usdc' | 'dai' | 'usdt', - ) { - const { - redemptionVaultWithMToken, - redemptionVaultWithSwapper, - redemptionVault, - owner, - mTBILL, - mBASIS, - mFONE, - mFoneToUsdDataFeed, - mBasisToUsdDataFeed, - mockedAggregatorMBasis, - mockedAggregatorMToken, - mockedAggregator, - dataFeed, - stableCoins, - liquidityProvider, - } = fixture; - - const token = stableCoins[tokenKey]; - - await redemptionVaultWithMToken.setRedemptionVault( - redemptionVaultWithSwapper.address, - ); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMToken, owner }, - token, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - token, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - token, - dataFeed.address, - 0, - true, - ); - - await redemptionVaultWithSwapper.addWaivedFeeAccount( - redemptionVaultWithMToken.address, - ); - - await setInstantFeeTest({ vault: redemptionVaultWithMToken, owner }, 0); - - await mintToken(mTBILL, liquidityProvider, 1_000_000); - await approveBase18( - liquidityProvider, - mTBILL, - redemptionVaultWithSwapper, - 1_000_000, - ); - await mintToken(token, redemptionVault, 1_000_000); - - return { - token, - redemptionVaultWithMToken, - redemptionVaultWithSwapper, - redemptionVault, - owner, - mTBILL, - mBASIS, - mFONE, - mFoneToUsdDataFeed, - mBasisToUsdDataFeed, - mockedAggregatorMBasis, - mockedAggregatorMToken, - mockedAggregator, - liquidityProvider, - }; - } - - for (const { key, label } of [ - { key: 'usdc6' as const, label: '6-dec' }, - { key: 'usdc' as const, label: '8-dec' }, - { key: 'dai' as const, label: '9-dec' }, - { key: 'usdt' as const, label: '18-dec' }, - ]) { - describe(`tokenOut ${label} (${key})`, () => { - it('succeeds with equal mBasis/mTBILL rates (mBasis=5, mTBILL=5, stable)', async () => { + describe('minReceiveAmount assertions', () => { + it('succeeds with exact minReceiveAmount (6-dec, mTokenLoan=1.05)', async () => { const fixture = await loadFixture(defaultDeploy); const { token, + amount, redemptionVaultWithMToken, owner, - mBASIS, - mFONE, - mockedAggregatorMBasis, - mockedAggregatorMToken, - } = await setupSwapperSwapPath(fixture, key); - - await setRoundData({ mockedAggregator: mockedAggregatorMBasis }, 5); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await mintToken(mFONE, owner, 10000); - await mintToken(mBASIS, redemptionVaultWithMToken, 10000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 10000); + mFoneRate, + tokenOutRate, + } = await setupCeilDivTest({ + fixture, + tokenKey: 'usdc6', + mTbillRate: 1.05, + isStable: true, + }); - const mBasisBefore = await mBASIS.balanceOf( - redemptionVaultWithMToken.address, + const expectedNative = computeExpectedTokenOut( + parseUnits(amount.toString()), + mFoneRate, + tokenOutRate, + TOKEN_DECIMALS['usdc6'], + 0, + true, ); + const expectedBase18 = expectedNative.mul( + BigNumber.from(10).pow(18 - TOKEN_DECIMALS['usdc6']), + ); + + const userTokenBefore = await token.balanceOf(owner.address); await redemptionVaultWithMToken .connect(owner) ['redeemInstant(address,uint256,uint256)']( token.address, - parseUnits('100'), - 0, + parseUnits(amount.toString()), + expectedBase18, ); - expect( - await mBASIS.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mBasisBefore); + const userTokenAfter = await token.balanceOf(owner.address); + expect(userTokenAfter.sub(userTokenBefore)).to.equal( + expectedNative, + ); }); - it('succeeds with clean-division rates (mBasis=6, mTBILL=3, stable)', async () => { + it('reverts when minReceiveAmount exceeds actual (6-dec, mTokenLoan=1.05)', async () => { const fixture = await loadFixture(defaultDeploy); const { token, + amount, redemptionVaultWithMToken, owner, - mBASIS, - mFONE, - mockedAggregatorMBasis, - mockedAggregatorMToken, - } = await setupSwapperSwapPath(fixture, key); + mFoneRate, + tokenOutRate, + } = await setupCeilDivTest({ + fixture, + tokenKey: 'usdc6', + mTbillRate: 1.05, + isStable: true, + }); + + const expectedNative = computeExpectedTokenOut( + parseUnits(amount.toString()), + mFoneRate, + tokenOutRate, + TOKEN_DECIMALS['usdc6'], + 0, + true, + ); + const tooHigh = expectedNative + .mul(BigNumber.from(10).pow(18 - TOKEN_DECIMALS['usdc6'])) + .add(1); + + await expect( + redemptionVaultWithMToken + .connect(owner) + ['redeemInstant(address,uint256,uint256)']( + token.address, + parseUnits(amount.toString()), + tooHigh, + ), + ).to.be.revertedWithCustomError( + redemptionVaultWithMToken, + 'SlippageExceeded', + ); + }); + }); + + describe('partial outer vault balance', () => { + it('succeeds with 30% tokenOut present in outer vault (6-dec, mTokenLoan=1.05)', async () => { + const fixture = await loadFixture(defaultDeploy); + const { + token, + amount, + redemptionVaultWithMToken, + owner, + mTokenLoan, + mFoneRate, + tokenOutRate, + } = await setupCeilDivTest({ + fixture, + tokenKey: 'usdc6', + mTbillRate: 1.05, + isStable: true, + }); - await setRoundData({ mockedAggregator: mockedAggregatorMBasis }, 6); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 3); - await mintToken(mFONE, owner, 10000); - await mintToken(mBASIS, redemptionVaultWithMToken, 10000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 10000); + const expectedNative = computeExpectedTokenOut( + parseUnits(amount.toString()), + mFoneRate, + tokenOutRate, + TOKEN_DECIMALS['usdc6'], + 0, + true, + ); + const thirtyPercent = expectedNative.mul(30).div(100); + await token.mint(redemptionVaultWithMToken.address, thirtyPercent); - const mBasisBefore = await mBASIS.balanceOf( + const mTbillBefore = await mTokenLoan.balanceOf( redemptionVaultWithMToken.address, ); + const userTokenBefore = await token.balanceOf(owner.address); await redemptionVaultWithMToken .connect(owner) ['redeemInstant(address,uint256,uint256)']( token.address, - parseUnits('100'), + parseUnits(amount.toString()), 0, ); expect( - await mBASIS.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mBasisBefore); + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + + const userTokenAfter = await token.balanceOf(owner.address); + expect(userTokenAfter.sub(userTokenBefore)).to.equal( + expectedNative, + ); }); - it('succeeds with remainder-producing swap (mBasis=1.05, mTBILL=1.03, stable)', async () => { + it('succeeds with 99% tokenOut present in outer vault (6-dec, mTokenLoan=1.05)', async () => { const fixture = await loadFixture(defaultDeploy); const { token, + amount, redemptionVaultWithMToken, owner, - mBASIS, - mFONE, - mockedAggregatorMBasis, - mockedAggregatorMToken, - } = await setupSwapperSwapPath(fixture, key); + mTokenLoan, + mFoneRate, + tokenOutRate, + } = await setupCeilDivTest({ + fixture, + tokenKey: 'usdc6', + mTbillRate: 1.05, + isStable: true, + }); - await setRoundData( - { mockedAggregator: mockedAggregatorMBasis }, - 1.05, + const expectedNative = computeExpectedTokenOut( + parseUnits(amount.toString()), + mFoneRate, + tokenOutRate, + TOKEN_DECIMALS['usdc6'], + 0, + true, ); - await setRoundData( - { mockedAggregator: mockedAggregatorMToken }, - 1.03, + const ninetyNinePercent = expectedNative.mul(99).div(100); + await token.mint( + redemptionVaultWithMToken.address, + ninetyNinePercent, ); - await mintToken(mFONE, owner, 10000); - await mintToken(mBASIS, redemptionVaultWithMToken, 10000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 10000); - const mBasisBefore = await mBASIS.balanceOf( + const mTbillBefore = await mTokenLoan.balanceOf( redemptionVaultWithMToken.address, ); + const userTokenBefore = await token.balanceOf(owner.address); await redemptionVaultWithMToken .connect(owner) ['redeemInstant(address,uint256,uint256)']( token.address, - parseUnits('100'), + parseUnits(amount.toString()), 0, ); expect( - await mBASIS.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mBasisBefore); + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + + const userTokenAfter = await token.balanceOf(owner.address); + expect(userTokenAfter.sub(userTokenBefore)).to.equal( + expectedNative, + ); }); + }); - it('succeeds with high divergent rates (mBasis=100, mTBILL=7, stable)', async () => { + describe('mTBILL rate variation', () => { + it('succeeds with high mTBILL rate (mTBILL=3.5, mTokenLoan=1.05, 6-dec)', async () => { const fixture = await loadFixture(defaultDeploy); const { token, + amount, redemptionVaultWithMToken, owner, - mBASIS, - mFONE, - mockedAggregatorMBasis, - mockedAggregatorMToken, - } = await setupSwapperSwapPath(fixture, key); + mTokenLoan, + mTokenToUsdDataFeed, + mockedAggregatorMTokenLoan, + tokenOutRate, + } = await setupCeilDivTest({ + fixture, + tokenKey: 'usdc6', + mTbillRate: 1.05, + isStable: true, + }); await setRoundData( - { mockedAggregator: mockedAggregatorMBasis }, - 100, + { mockedAggregator: mockedAggregatorMTokenLoan }, + 3.5, ); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 7); - await mintToken(mFONE, owner, 10000); - await mintToken(mBASIS, redemptionVaultWithMToken, 10000); - await approveBase18(owner, mFONE, redemptionVaultWithMToken, 10000); + const mFoneRate = await mTokenToUsdDataFeed.getDataInBase18(); - const mBasisBefore = await mBASIS.balanceOf( + const mTbillBefore = await mTokenLoan.balanceOf( redemptionVaultWithMToken.address, ); + const userTokenBefore = await token.balanceOf(owner.address); await redemptionVaultWithMToken .connect(owner) ['redeemInstant(address,uint256,uint256)']( token.address, - parseUnits('100'), + parseUnits(amount.toString()), 0, ); expect( - await mBASIS.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mBasisBefore); + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); + + const expected = computeExpectedTokenOut( + parseUnits(amount.toString()), + mFoneRate, + tokenOutRate, + TOKEN_DECIMALS['usdc6'], + 0, + true, + ); + const userTokenAfter = await token.balanceOf(owner.address); + expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); }); - it('succeeds with near-boundary rates (mBasis=1.00000003, mTBILL=1.00000007, stable)', async () => { + it('succeeds with low mTBILL rate (mTBILL=0.5, mTokenLoan=1.05, 6-dec)', async () => { const fixture = await loadFixture(defaultDeploy); const { token, + amount, redemptionVaultWithMToken, owner, - mBASIS, - mFONE, - mockedAggregatorMBasis, - mockedAggregatorMToken, - } = await setupSwapperSwapPath(fixture, key); + mTokenLoan, + mTokenToUsdDataFeed, + mockedAggregatorMTokenLoan, + tokenOutRate, + } = await setupCeilDivTest({ + fixture, + tokenKey: 'usdc6', + mTbillRate: 1.05, + isStable: true, + }); await setRoundData( - { mockedAggregator: mockedAggregatorMBasis }, - 1.00000003, - ); - await setRoundData( - { mockedAggregator: mockedAggregatorMToken }, - 1.00000007, - ); - await mintToken(mFONE, owner, 100000); - await mintToken(mBASIS, redemptionVaultWithMToken, 100000); - await approveBase18( - owner, - mFONE, - redemptionVaultWithMToken, - 100000, + { mockedAggregator: mockedAggregatorMTokenLoan }, + 0.5, ); + const mFoneRate = await mTokenToUsdDataFeed.getDataInBase18(); - const mBasisBefore = await mBASIS.balanceOf( + const mTbillBefore = await mTokenLoan.balanceOf( redemptionVaultWithMToken.address, ); + const userTokenBefore = await token.balanceOf(owner.address); await redemptionVaultWithMToken .connect(owner) ['redeemInstant(address,uint256,uint256)']( token.address, - parseUnits('10000'), + parseUnits(amount.toString()), 0, ); expect( - await mBASIS.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mBasisBefore); - }); - }); - } - }); - - describe('with outer vault fee enabled', () => { - it('succeeds with 1% fee, usdc6 (6-dec), mTBILL=1.05, stable', async () => { - const fixture = await loadFixture(defaultDeploy); - const { token, amount, redemptionVaultWithMToken, owner, mTBILL } = - await setupCeilDivTest({ - fixture, - tokenKey: 'usdc6', - mTbillRate: 1.05, - isStable: true, - }); - - await setInstantFeeTest( - { vault: redemptionVaultWithMToken, owner }, - 100, - ); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - expect(await token.balanceOf(owner.address)).to.be.gt(userTokenBefore); - }); - - it('succeeds with 1% fee, usdc (8-dec), mTBILL=1.05, stable', async () => { - const fixture = await loadFixture(defaultDeploy); - const { token, amount, redemptionVaultWithMToken, owner, mTBILL } = - await setupCeilDivTest({ - fixture, - tokenKey: 'usdc', - mTbillRate: 1.05, - isStable: true, - }); - - await setInstantFeeTest( - { vault: redemptionVaultWithMToken, owner }, - 100, - ); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - expect(await token.balanceOf(owner.address)).to.be.gt(userTokenBefore); - }); - - it('succeeds with 1% fee, usdt (18-dec), mTBILL=1.05, stable', async () => { - const fixture = await loadFixture(defaultDeploy); - const { token, amount, redemptionVaultWithMToken, owner, mTBILL } = - await setupCeilDivTest({ - fixture, - tokenKey: 'usdt', - mTbillRate: 1.05, - isStable: true, - }); - - await setInstantFeeTest( - { vault: redemptionVaultWithMToken, owner }, - 100, - ); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - expect(await token.balanceOf(owner.address)).to.be.gt(userTokenBefore); - }); - - it('succeeds with 1% fee, usdc6 (6-dec), mTBILL=100, stable', async () => { - const fixture = await loadFixture(defaultDeploy); - const { token, amount, redemptionVaultWithMToken, owner, mTBILL } = - await setupCeilDivTest({ - fixture, - tokenKey: 'usdc6', - mTbillRate: 100, - isStable: true, - }); - - await setInstantFeeTest( - { vault: redemptionVaultWithMToken, owner }, - 100, - ); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - }); - - it('succeeds with 1% fee, usdc (8-dec), non-stable mTBILL=1.05 tokenOut=1.03', async () => { - const fixture = await loadFixture(defaultDeploy); - const { token, amount, redemptionVaultWithMToken, owner, mTBILL } = - await setupCeilDivTest({ - fixture, - tokenKey: 'usdc', - mTbillRate: 1.05, - isStable: false, - tokenOutRate: 1.03, - }); - - await setInstantFeeTest( - { vault: redemptionVaultWithMToken, owner }, - 100, - ); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - expect(await token.balanceOf(owner.address)).to.be.gt(userTokenBefore); - }); - - it('succeeds with 1% fee, usdc6 (6-dec), near-boundary mTBILL=1.00000003', async () => { - const fixture = await loadFixture(defaultDeploy); - const { token, redemptionVaultWithMToken, owner, mTBILL } = - await setupCeilDivTest({ - fixture, - tokenKey: 'usdc6', - mTbillRate: 1.00000003, - isStable: true, - redeemAmount: 10000, - }); - - await setInstantFeeTest( - { vault: redemptionVaultWithMToken, owner }, - 100, - ); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits('10000'), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - }); - }); - - describe('minReceiveAmount assertions', () => { - it('succeeds with exact minReceiveAmount (6-dec, mTBILL=1.05)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - amount, - redemptionVaultWithMToken, - owner, - mFoneRate, - tokenOutRate, - } = await setupCeilDivTest({ - fixture, - tokenKey: 'usdc6', - mTbillRate: 1.05, - isStable: true, - }); - - const expectedNative = computeExpectedTokenOut( - parseUnits(amount.toString()), - mFoneRate, - tokenOutRate, - TOKEN_DECIMALS['usdc6'], - 0, - true, - ); - const expectedBase18 = expectedNative.mul( - BigNumber.from(10).pow(18 - TOKEN_DECIMALS['usdc6']), - ); - - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - expectedBase18, - ); - - const userTokenAfter = await token.balanceOf(owner.address); - expect(userTokenAfter.sub(userTokenBefore)).to.equal(expectedNative); - }); - - it('reverts when minReceiveAmount exceeds actual (6-dec, mTBILL=1.05)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - amount, - redemptionVaultWithMToken, - owner, - mFoneRate, - tokenOutRate, - } = await setupCeilDivTest({ - fixture, - tokenKey: 'usdc6', - mTbillRate: 1.05, - isStable: true, - }); + await mTokenLoan.balanceOf(redemptionVaultWithMToken.address), + ).to.be.lt(mTbillBefore); - const expectedNative = computeExpectedTokenOut( - parseUnits(amount.toString()), - mFoneRate, - tokenOutRate, - TOKEN_DECIMALS['usdc6'], - 0, - true, - ); - const tooHigh = expectedNative - .mul(BigNumber.from(10).pow(18 - TOKEN_DECIMALS['usdc6'])) - .add(1); - - await expect( - redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, + const expected = computeExpectedTokenOut( parseUnits(amount.toString()), - tooHigh, - ), - ).to.be.revertedWith('RVMT: minReceiveAmount > actual'); - }); - }); - - describe('partial outer vault balance', () => { - it('succeeds with 30% tokenOut present in outer vault (6-dec, mTBILL=1.05)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - amount, - redemptionVaultWithMToken, - owner, - mTBILL, - mFoneRate, - tokenOutRate, - } = await setupCeilDivTest({ - fixture, - tokenKey: 'usdc6', - mTbillRate: 1.05, - isStable: true, - }); - - const expectedNative = computeExpectedTokenOut( - parseUnits(amount.toString()), - mFoneRate, - tokenOutRate, - TOKEN_DECIMALS['usdc6'], - 0, - true, - ); - const thirtyPercent = expectedNative.mul(30).div(100); - await token.mint(redemptionVaultWithMToken.address, thirtyPercent); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - - const userTokenAfter = await token.balanceOf(owner.address); - expect(userTokenAfter.sub(userTokenBefore)).to.equal(expectedNative); - }); - - it('succeeds with 99% tokenOut present in outer vault (6-dec, mTBILL=1.05)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - amount, - redemptionVaultWithMToken, - owner, - mTBILL, - mFoneRate, - tokenOutRate, - } = await setupCeilDivTest({ - fixture, - tokenKey: 'usdc6', - mTbillRate: 1.05, - isStable: true, - }); - - const expectedNative = computeExpectedTokenOut( - parseUnits(amount.toString()), - mFoneRate, - tokenOutRate, - TOKEN_DECIMALS['usdc6'], - 0, - true, - ); - const ninetyNinePercent = expectedNative.mul(99).div(100); - await token.mint(redemptionVaultWithMToken.address, ninetyNinePercent); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - - const userTokenAfter = await token.balanceOf(owner.address); - expect(userTokenAfter.sub(userTokenBefore)).to.equal(expectedNative); - }); - }); - - describe('mFONE rate variation', () => { - it('succeeds with high mFONE rate (mFONE=3.5, mTBILL=1.05, 6-dec)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - amount, - redemptionVaultWithMToken, - owner, - mTBILL, - mFoneToUsdDataFeed, - mockedAggregatorMFone, - tokenOutRate, - } = await setupCeilDivTest({ - fixture, - tokenKey: 'usdc6', - mTbillRate: 1.05, - isStable: true, - }); - - await setRoundData({ mockedAggregator: mockedAggregatorMFone }, 3.5); - const mFoneRate = await mFoneToUsdDataFeed.getDataInBase18(); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - - const expected = computeExpectedTokenOut( - parseUnits(amount.toString()), - mFoneRate, - tokenOutRate, - TOKEN_DECIMALS['usdc6'], - 0, - true, - ); - const userTokenAfter = await token.balanceOf(owner.address); - expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); - }); - - it('succeeds with low mFONE rate (mFONE=0.5, mTBILL=1.05, 6-dec)', async () => { - const fixture = await loadFixture(defaultDeploy); - const { - token, - amount, - redemptionVaultWithMToken, - owner, - mTBILL, - mFoneToUsdDataFeed, - mockedAggregatorMFone, - tokenOutRate, - } = await setupCeilDivTest({ - fixture, - tokenKey: 'usdc6', - mTbillRate: 1.05, - isStable: true, + mFoneRate, + tokenOutRate, + TOKEN_DECIMALS['usdc6'], + 0, + true, + ); + const userTokenAfter = await token.balanceOf(owner.address); + expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); + }); }); - - await setRoundData({ mockedAggregator: mockedAggregatorMFone }, 0.5); - const mFoneRate = await mFoneToUsdDataFeed.getDataInBase18(); - - const mTbillBefore = await mTBILL.balanceOf( - redemptionVaultWithMToken.address, - ); - const userTokenBefore = await token.balanceOf(owner.address); - - await redemptionVaultWithMToken - .connect(owner) - ['redeemInstant(address,uint256,uint256)']( - token.address, - parseUnits(amount.toString()), - 0, - ); - - expect( - await mTBILL.balanceOf(redemptionVaultWithMToken.address), - ).to.be.lt(mTbillBefore); - - const expected = computeExpectedTokenOut( - parseUnits(amount.toString()), - mFoneRate, - tokenOutRate, - TOKEN_DECIMALS['usdc6'], - 0, - true, - ); - const userTokenAfter = await token.balanceOf(owner.address); - expect(userTokenAfter.sub(userTokenBefore)).to.equal(expected); }); }); - }); -}); + }, +); diff --git a/test/unit/RedemptionVaultWithMorpho.test.ts b/test/unit/RedemptionVaultWithMorpho.test.ts index 1517a731..628c5ef4 100644 --- a/test/unit/RedemptionVaultWithMorpho.test.ts +++ b/test/unit/RedemptionVaultWithMorpho.test.ts @@ -1,2774 +1,1206 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; import { constants } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; import { ethers } from 'hardhat'; +import { + baseInitParamsRv, + redemptionVaultSuits, +} from './suits/redemption-vault.suits'; + import { encodeFnSelector } from '../../helpers/utils'; import { - ManageableVaultTester__factory, + RedemptionVaultWithMorpho__factory, RedemptionVaultWithMorphoTest__factory, } from '../../typechain-types'; -import { acErrors, blackList, greenList } from '../common/ac.helpers'; +import { acErrors } from '../common/ac.helpers'; import { approveBase18, mintToken, pauseVaultFn, + validateImplementation, } from '../common/common.helpers'; import { setRoundData } from '../common/data-feed.helpers'; import { defaultDeploy } from '../common/fixtures'; import { addPaymentTokenTest, setInstantFeeTest, - setMinAmountTest, - setInstantDailyLimitTest, - addWaivedFeeAccountTest, - removeWaivedFeeAccountTest, - setVariabilityToleranceTest, - removePaymentTokenTest, - withdrawTest, - changeTokenFeeTest, - changeTokenAllowanceTest, + setWaivedFeeAccountTest, } from '../common/manageable-vault.helpers'; import { setMorphoVaultTest, removeMorphoVaultTest, } from '../common/redemption-vault-morpho.helpers'; import { - approveRedeemRequestTest, - redeemFiatRequestTest, redeemInstantTest, - redeemRequestTest, - rejectRedeemRequestTest, - safeApproveRedeemRequestTest, - setFiatAdditionalFeeTest, - setMinFiatRedeemAmountTest, + setPreferLoanLiquidityTest, + setLoanLpTest, } from '../common/redemption-vault.helpers'; -import { sanctionUser } from '../common/with-sanctions-list.helpers'; - -describe('RedemptionVaultWithMorpho', function () { - it('deployment', async () => { - const { - redemptionVaultWithMorpho, - morphoVaultMock, - stableCoins, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - roles, - } = await loadFixture(defaultDeploy); - - expect(await redemptionVaultWithMorpho.mToken()).eq(mTBILL.address); - - expect(await redemptionVaultWithMorpho.ONE_HUNDRED_PERCENT()).eq('10000'); - - expect(await redemptionVaultWithMorpho.paused()).eq(false); - - expect(await redemptionVaultWithMorpho.tokensReceiver()).eq( - tokensReceiver.address, - ); - expect(await redemptionVaultWithMorpho.feeReceiver()).eq( - feeReceiver.address, - ); - - expect(await redemptionVaultWithMorpho.minAmount()).eq(1000); - expect(await redemptionVaultWithMorpho.minFiatRedeemAmount()).eq(1000); - - expect(await redemptionVaultWithMorpho.instantFee()).eq('100'); - - expect(await redemptionVaultWithMorpho.instantDailyLimit()).eq( - parseUnits('100000'), - ); - - expect(await redemptionVaultWithMorpho.mTokenDataFeed()).eq( - mTokenToUsdDataFeed.address, - ); - expect(await redemptionVaultWithMorpho.variationTolerance()).eq(1); - - expect(await redemptionVaultWithMorpho.vaultRole()).eq( - roles.tokenRoles.mTBILL.redemptionVaultAdmin, - ); - - expect(await redemptionVaultWithMorpho.MANUAL_FULLFILMENT_TOKEN()).eq( - ethers.constants.AddressZero, - ); - +import { initializeRvWithMorpho } from '../common/vault-initializer.helpers'; + +redemptionVaultSuits( + 'RedemptionVaultWithMorpho', + defaultDeploy, + { + createNew: async (owner: SignerWithAddress) => + new RedemptionVaultWithMorphoTest__factory(owner).deploy(), + key: 'redemptionVaultWithMorpho', + }, + async (fixture) => { + const { redemptionVaultWithMorpho, stableCoins, morphoVaultMock } = fixture; expect( await redemptionVaultWithMorpho.morphoVaults(stableCoins.usdc.address), ).eq(morphoVaultMock.address); - }); - - it('failing deployment', async () => { - const { - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - accessControl, - mockedSanctionsList, - owner, - } = await loadFixture(defaultDeploy); - - const redemptionVaultWithMorpho = - await new RedemptionVaultWithMorphoTest__factory(owner).deploy(); - - await expect( - redemptionVaultWithMorpho.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - { - fiatAdditionalFee: 10000, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: parseUnits('100'), - }, - constants.AddressZero, - ), - ).to.be.reverted; - }); - - describe('initialization', () => { - it('should fail: call initialize() when already initialized', async () => { - const { redemptionVaultWithMorpho } = await loadFixture(defaultDeploy); - - await expect( - redemptionVaultWithMorpho.initialize( - constants.AddressZero, - { - mToken: constants.AddressZero, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: constants.AddressZero, - tokensReceiver: constants.AddressZero, - }, - { - instantFee: 0, - instantDailyLimit: 0, - }, - constants.AddressZero, - 0, - 0, - { - fiatAdditionalFee: 0, - fiatFlatFee: 0, - minFiatRedeemAmount: 0, - }, - constants.AddressZero, - ), - ).revertedWith('Initializable: contract is already initialized'); - }); - - it('should fail: call with initializing == false', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initializeWithoutInitializer( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('Initializable: contract is not initializing'); - }); - }); - - describe('setMorphoVault()', () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithMorpho, - owner, - regularAccounts, - stableCoins, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await setMorphoVaultTest( - { redemptionVault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: zero token address', async () => { - const { redemptionVaultWithMorpho, owner, morphoVaultMock } = - await loadFixture(defaultDeploy); - - await setMorphoVaultTest( - { redemptionVault: redemptionVaultWithMorpho, owner }, - constants.AddressZero, - morphoVaultMock.address, - { - revertMessage: 'zero address', - }, - ); - }); - - it('should fail: zero vault address', async () => { - const { redemptionVaultWithMorpho, owner, stableCoins } = - await loadFixture(defaultDeploy); - - await setMorphoVaultTest( - { redemptionVault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc.address, - constants.AddressZero, - { - revertMessage: 'zero address', - }, - ); - }); - - it('should fail: asset mismatch', async () => { - const { redemptionVaultWithMorpho, owner, stableCoins, morphoVaultMock } = - await loadFixture(defaultDeploy); - - await setMorphoVaultTest( - { redemptionVault: redemptionVaultWithMorpho, owner }, - stableCoins.dai.address, - morphoVaultMock.address, - { - revertMessage: 'RVM: asset mismatch', - }, - ); - }); - - it('call from address with vault admin role', async () => { - const { redemptionVaultWithMorpho, owner, stableCoins, morphoVaultMock } = - await loadFixture(defaultDeploy); - - await setMorphoVaultTest( - { redemptionVault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc.address, - morphoVaultMock.address, - ); - }); - }); - - describe('removeMorphoVault()', () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithMorpho, owner, regularAccounts, stableCoins } = - await loadFixture(defaultDeploy); - - await removeMorphoVaultTest( - { redemptionVault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc.address, - { - from: regularAccounts[0], - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: vault not set', async () => { - const { redemptionVaultWithMorpho, owner, stableCoins } = - await loadFixture(defaultDeploy); - - await removeMorphoVaultTest( - { redemptionVault: redemptionVaultWithMorpho, owner }, - stableCoins.dai.address, - { - revertMessage: 'RVM: vault not set', - }, - ); - }); - - it('call from address with vault admin role', async () => { - const { redemptionVaultWithMorpho, owner, stableCoins } = - await loadFixture(defaultDeploy); - - await removeMorphoVaultTest( - { redemptionVault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc.address, - ); - }); - }); - - describe('setMinAmount()', () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithMorpho, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await setMinAmountTest( - { vault: redemptionVaultWithMorpho, owner: regularAccounts[0] }, - 1, - { revertMessage: 'WMAC: hasnt role', from: regularAccounts[0] }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMorpho } = await loadFixture( - defaultDeploy, - ); - await setMinAmountTest({ vault: redemptionVaultWithMorpho, owner }, 1); - }); - }); - - describe('setMinFiatRedeemAmount()', () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithMorpho, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await setMinFiatRedeemAmountTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner: regularAccounts[0], - }, - 1, - { revertMessage: 'WMAC: hasnt role', from: regularAccounts[0] }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMorpho } = await loadFixture( - defaultDeploy, - ); - await setMinFiatRedeemAmountTest( - { redemptionVault: redemptionVaultWithMorpho, owner }, - 1, - ); - }); - }); - - describe('setFiatAdditionalFee()', () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithMorpho, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await setFiatAdditionalFeeTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner: regularAccounts[0], - }, - 1, - { revertMessage: 'WMAC: hasnt role', from: regularAccounts[0] }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMorpho } = await loadFixture( - defaultDeploy, - ); - await setFiatAdditionalFeeTest( - { redemptionVault: redemptionVaultWithMorpho, owner }, - 1, - ); - }); - }); - - describe('setInstantDailyLimit()', () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithMorpho, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await setInstantDailyLimitTest( - { vault: redemptionVaultWithMorpho, owner: regularAccounts[0] }, - 1, - { revertMessage: 'WMAC: hasnt role', from: regularAccounts[0] }, - ); - }); - - it('should fail: try to set 0 limit', async () => { - const { owner, redemptionVaultWithMorpho } = await loadFixture( - defaultDeploy, - ); - await setInstantDailyLimitTest( - { vault: redemptionVaultWithMorpho, owner }, - constants.Zero, - { revertMessage: 'MV: limit zero' }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMorpho } = await loadFixture( - defaultDeploy, - ); - await setInstantDailyLimitTest( - { vault: redemptionVaultWithMorpho, owner }, - 1, - ); - }); - }); - - describe('addPaymentToken()', () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithMorpho, - regularAccounts, - dataFeed, - stableCoins, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner: regularAccounts[0] }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - constants.MaxUint256, - { revertMessage: 'WMAC: hasnt role', from: regularAccounts[0] }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMorpho, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - }); - }); - - describe('removePaymentToken()', () => { - it('should fail: call from address without vault admin role', async () => { - const { - owner, - redemptionVaultWithMorpho, - regularAccounts, - stableCoins, - dataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await removePaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner: regularAccounts[0] }, - stableCoins.usdc, - { revertMessage: 'WMAC: hasnt role', from: regularAccounts[0] }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMorpho, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await removePaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - ); - }); - }); - - describe('addWaivedFeeAccount()', () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithMorpho, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithMorpho, owner: regularAccounts[0] }, - regularAccounts[1].address, - { revertMessage: 'WMAC: hasnt role', from: regularAccounts[0] }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMorpho, regularAccounts } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithMorpho, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('removeWaivedFeeAccount()', () => { - it('should fail: call from address without vault admin role', async () => { - const { owner, redemptionVaultWithMorpho, regularAccounts } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithMorpho, owner }, - regularAccounts[0].address, - ); - await removeWaivedFeeAccountTest( - { vault: redemptionVaultWithMorpho, owner: regularAccounts[0] }, - regularAccounts[0].address, - { revertMessage: 'WMAC: hasnt role', from: regularAccounts[0] }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMorpho, regularAccounts } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithMorpho, owner }, - regularAccounts[0].address, - ); - await removeWaivedFeeAccountTest( - { vault: redemptionVaultWithMorpho, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('setFee()', () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithMorpho, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await setInstantFeeTest( - { vault: redemptionVaultWithMorpho, owner: regularAccounts[0] }, - 1, - { revertMessage: 'WMAC: hasnt role', from: regularAccounts[0] }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMorpho } = await loadFixture( - defaultDeploy, - ); - await setInstantFeeTest({ vault: redemptionVaultWithMorpho, owner }, 1); - }); - }); - - describe('setVariabilityTolerance()', () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithMorpho, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await setVariabilityToleranceTest( - { vault: redemptionVaultWithMorpho, owner: regularAccounts[0] }, - 1, - { revertMessage: 'WMAC: hasnt role', from: regularAccounts[0] }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMorpho } = await loadFixture( - defaultDeploy, - ); - await setVariabilityToleranceTest( - { vault: redemptionVaultWithMorpho, owner }, - 1, - ); - }); - }); - - describe('withdrawToken()', () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithMorpho, stableCoins, regularAccounts } = - await loadFixture(defaultDeploy); - await withdrawTest( - { vault: redemptionVaultWithMorpho, owner: regularAccounts[0] }, - stableCoins.usdc, - 1, - regularAccounts[0], - { revertMessage: 'WMAC: hasnt role', from: regularAccounts[0] }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMorpho, stableCoins } = - await loadFixture(defaultDeploy); - await mintToken(stableCoins.usdc, redemptionVaultWithMorpho, 1); - await withdrawTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - 1, - owner, - ); - }); - }); - - describe('freeFromMinAmount()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithMorpho, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithMorpho - .connect(regularAccounts[0]) - .freeFromMinAmount(regularAccounts[0].address, true), - ).revertedWith('WMAC: hasnt role'); - }); - - it('should not fail', async () => { - const { owner, redemptionVaultWithMorpho } = await loadFixture( - defaultDeploy, - ); - - expect( - await redemptionVaultWithMorpho.isFreeFromMinAmount(owner.address), - ).eq(false); - await redemptionVaultWithMorpho.freeFromMinAmount(owner.address, true); - expect( - await redemptionVaultWithMorpho.isFreeFromMinAmount(owner.address), - ).eq(true); - }); - }); - - describe('changeTokenAllowance()', () => { - it('should fail: call from address without vault admin role', async () => { - const { - owner, - redemptionVaultWithMorpho, - regularAccounts, - stableCoins, - dataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithMorpho, owner: regularAccounts[0] }, - stableCoins.usdc.address, - 100, - { revertMessage: 'WMAC: hasnt role', from: regularAccounts[0] }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMorpho, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc.address, - 100, - ); - }); - }); - - describe('changeTokenFee()', () => { - it('should fail: call from address without vault admin role', async () => { - const { - owner, - redemptionVaultWithMorpho, - regularAccounts, - stableCoins, - dataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: redemptionVaultWithMorpho, owner: regularAccounts[0] }, - stableCoins.usdc.address, - 100, - { revertMessage: 'WMAC: hasnt role', from: regularAccounts[0] }, - ); - }); - - it('call from address with vault admin role', async () => { - const { owner, redemptionVaultWithMorpho, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc.address, - 100, - ); - }); - }); - - describe('checkAndRedeemMorpho()', () => { - it('should not withdraw from Morpho when contract has enough balance', async () => { - const { redemptionVaultWithMorpho, stableCoins, morphoVaultMock } = - await loadFixture(defaultDeploy); - - const usdcAmount = parseUnits('1000', 8); - await stableCoins.usdc.mint( - redemptionVaultWithMorpho.address, - usdcAmount, - ); - - const balanceBefore = await stableCoins.usdc.balanceOf( - redemptionVaultWithMorpho.address, - ); - const sharesBefore = await morphoVaultMock.balanceOf( - redemptionVaultWithMorpho.address, - ); - - await redemptionVaultWithMorpho.checkAndRedeemMorpho( - stableCoins.usdc.address, - parseUnits('500', 8), - ); - - const balanceAfter = await stableCoins.usdc.balanceOf( - redemptionVaultWithMorpho.address, - ); - const sharesAfter = await morphoVaultMock.balanceOf( - redemptionVaultWithMorpho.address, - ); - expect(balanceAfter).to.equal(balanceBefore); - expect(sharesAfter).to.equal(sharesBefore); - }); - - it('should withdraw missing amount from Morpho', async () => { - const { redemptionVaultWithMorpho, stableCoins, morphoVaultMock } = - await loadFixture(defaultDeploy); - - // Vault has 500 USDC, needs 1000 - const initialUsdc = parseUnits('500', 8); - await stableCoins.usdc.mint( - redemptionVaultWithMorpho.address, - initialUsdc, - ); - - // Vault has 600 Morpho shares (1:1 exchange rate by default) - const sharesAmount = parseUnits('600', 8); - await morphoVaultMock.mint( - redemptionVaultWithMorpho.address, - sharesAmount, - ); - - await redemptionVaultWithMorpho.checkAndRedeemMorpho( - stableCoins.usdc.address, - parseUnits('1000', 8), - ); - - // Vault should now have 1000 USDC (500 original + 500 withdrawn from Morpho) - const usdcAfter = await stableCoins.usdc.balanceOf( - redemptionVaultWithMorpho.address, - ); - expect(usdcAfter).to.equal(parseUnits('1000', 8)); - - // Share balance should decrease by 500 (1:1 rate) - const sharesAfter = await morphoVaultMock.balanceOf( - redemptionVaultWithMorpho.address, - ); - expect(sharesAfter).to.equal(parseUnits('100', 8)); - }); - - it('should revert when token not vault asset', async () => { - const { redemptionVaultWithMorpho, stableCoins } = await loadFixture( - defaultDeploy, - ); - - // DAI is not the Morpho vault's underlying asset (USDC is) - await expect( - redemptionVaultWithMorpho.checkAndRedeemMorpho( - stableCoins.dai.address, - parseUnits('1000', 9), - ), - ).to.be.revertedWith('RVM: no vault for token'); - }); - - it('should revert when contract has insufficient shares', async () => { - const { redemptionVaultWithMorpho, stableCoins, morphoVaultMock } = - await loadFixture(defaultDeploy); - - // Vault has 200 USDC, needs 1000 - await stableCoins.usdc.mint( - redemptionVaultWithMorpho.address, - parseUnits('200', 8), - ); - - // Vault has only 300 shares (not enough for 800 missing at 1:1 rate) - await morphoVaultMock.mint( - redemptionVaultWithMorpho.address, - parseUnits('300', 8), - ); - - await expect( - redemptionVaultWithMorpho.checkAndRedeemMorpho( - stableCoins.usdc.address, - parseUnits('1000', 8), - ), - ).to.be.revertedWith('RVM: insufficient shares'); - }); - - it('should revert when Morpho vault has insufficient underlying liquidity', async () => { - const { redemptionVaultWithMorpho, stableCoins, morphoVaultMock } = - await loadFixture(defaultDeploy); - - // Vault needs to withdraw from Morpho - await stableCoins.usdc.mint( - redemptionVaultWithMorpho.address, - parseUnits('200', 8), - ); - - // Vault has enough shares - await morphoVaultMock.mint( - redemptionVaultWithMorpho.address, - parseUnits('1000', 8), - ); - - // Drain the mock's USDC - const mockBalance = await stableCoins.usdc.balanceOf( - morphoVaultMock.address, - ); - await morphoVaultMock.withdrawAdmin( - stableCoins.usdc.address, - ( - await ethers.getSigners() - )[10].address, - mockBalance, - ); - - await expect( - redemptionVaultWithMorpho.checkAndRedeemMorpho( - stableCoins.usdc.address, - parseUnits('1000', 8), - ), - ).to.be.revertedWith('MorphoVaultMock: InsufficientLiquidity'); - }); - - it('should withdraw correctly with non-1:1 exchange rate (shares worth more)', async () => { - const { redemptionVaultWithMorpho, stableCoins, morphoVaultMock } = - await loadFixture(defaultDeploy); - - // Set exchange rate: 1 share = 1.05 underlying (5% interest accrued) - await morphoVaultMock.setExchangeRate(parseUnits('1.05', 18)); - - // Vault has 200 USDC, needs 1000 → missing 800 - await stableCoins.usdc.mint( - redemptionVaultWithMorpho.address, - parseUnits('200', 8), - ); - - // At 1.05 rate, 800 assets needs ceil(800 / 1.05) ≈ 762 shares - // Mint 800 shares (more than enough) - await morphoVaultMock.mint( - redemptionVaultWithMorpho.address, - parseUnits('800', 8), - ); - - const sharesBefore = await morphoVaultMock.balanceOf( - redemptionVaultWithMorpho.address, - ); - - await redemptionVaultWithMorpho.checkAndRedeemMorpho( - stableCoins.usdc.address, - parseUnits('1000', 8), - ); - - const usdcAfter = await stableCoins.usdc.balanceOf( - redemptionVaultWithMorpho.address, - ); - expect(usdcAfter).to.equal(parseUnits('1000', 8)); - - const sharesAfter = await morphoVaultMock.balanceOf( - redemptionVaultWithMorpho.address, - ); - // Shares burned should be less than 800 because each share is worth 1.05 - expect(sharesAfter).to.be.gt(0); - const sharesBurned = sharesBefore.sub(sharesAfter); - expect(sharesBurned).to.be.lt(parseUnits('800', 8)); - }); - - it('should revert with insufficient shares at non-1:1 exchange rate', async () => { - const { redemptionVaultWithMorpho, stableCoins, morphoVaultMock } = - await loadFixture(defaultDeploy); - - // Set exchange rate: 1 share = 0.95 underlying (loss scenario) - await morphoVaultMock.setExchangeRate(parseUnits('0.95', 18)); - - // Vault has 200 USDC, needs 1000 → missing 800 - await stableCoins.usdc.mint( - redemptionVaultWithMorpho.address, - parseUnits('200', 8), - ); - - // At 0.95 rate, 800 assets needs ceil(800 / 0.95) ≈ 843 shares - // Mint only 800 shares (not enough at this rate) - await morphoVaultMock.mint( - redemptionVaultWithMorpho.address, - parseUnits('800', 8), - ); - - await expect( - redemptionVaultWithMorpho.checkAndRedeemMorpho( - stableCoins.usdc.address, - parseUnits('1000', 8), - ), - ).to.be.revertedWith('RVM: insufficient shares'); - }); - }); - - describe('redeemInstant()', () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 0, - { - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemInstant(address,uint256,uint256)', - ); - await pauseVaultFn(redemptionVaultWithMorpho, selector); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'ERC20: burn amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mockedAggregatorMToken, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18( - owner, - stableCoins.usdc, - redemptionVaultWithMorpho, - 10, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await mintToken(mTBILL, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minAmount', async () => { - const { - redemptionVaultWithMorpho, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 100_000); - - await setMinAmountTest( - { vault: redemptionVaultWithMorpho, owner }, - 100_000, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'RV: amount < min', - }, - ); - }); - - it('should fail: if exceeds token allowance', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(mTBILL, owner, 100_000); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc.address, - 100, - ); - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 100_000); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - - it('should fail: if daily limit exceeded', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(mTBILL, owner, 100_000); - await setInstantDailyLimitTest( - { vault: redemptionVaultWithMorpho, owner }, - 1000, - ); - - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 100_000); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'MV: exceed limit', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 10000, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'RV: amountMTokenIn < fee', - }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithMorpho.setGreenlistEnable(true); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedSanctionsList, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - // ── Happy path tests ───────────────────────────────────────────────── - - it('redeem 100 mTBILL when vault has enough USDC (no Morpho needed)', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithMorpho, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - const sharesBefore = await morphoVaultMock.balanceOf( - redemptionVaultWithMorpho.address, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - - // Share balance should not change - const sharesAfter = await morphoVaultMock.balanceOf( - redemptionVaultWithMorpho.address, - ); - expect(sharesAfter).to.equal(sharesBefore); - }); - - it('redeem 1000 mTBILL when vault has no USDC but has Morpho shares', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - // Mint shares to vault (enough for redemption at 1:1 rate) - await morphoVaultMock.mint( - redemptionVaultWithMorpho.address, - parseUnits('9900', 8), - ); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: redemptionVaultWithMorpho, owner }, 0); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - const sharesBefore = await morphoVaultMock.balanceOf( - redemptionVaultWithMorpho.address, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1000, - ); - - const sharesAfter = await morphoVaultMock.balanceOf( - redemptionVaultWithMorpho.address, - ); - - // Shares should decrease - expect(sharesAfter).to.be.lt(sharesBefore); - }); - - it('redeem 1000 mTBILL when vault has 100 USDC and sufficient shares (partial Morpho)', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - // Vault has 100 USDC + shares - await mintToken(stableCoins.usdc, redemptionVaultWithMorpho, 100); - await morphoVaultMock.mint( - redemptionVaultWithMorpho.address, - parseUnits('9900', 8), - ); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1000, - ); - }); - - it('redeem 1000 mTBILL with different prices (stable 1.03$, mToken 5$) and partial Morpho', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithMorpho, 100); - await morphoVaultMock.mint( - redemptionVaultWithMorpho.address, - parseUnits('15000', 8), - ); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redemptionVaultWithMorpho.freeFromMinAmount(owner.address, true); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1000, - ); - }); - - it('redeem 1000 mTBILL with waived fee and Morpho withdrawal', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithMorpho, 100); - await morphoVaultMock.mint( - redemptionVaultWithMorpho.address, - parseUnits('15000', 8), - ); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithMorpho, owner }, - owner.address, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - waivedFee: true, - }, - stableCoins.usdc, - 1000, - ); - }); - - it('should fail: insufficient shares during redeemInstant', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - // Vault has no USDC and only 10 shares (not enough for 1000 mTBILL redemption) - await morphoVaultMock.mint( - redemptionVaultWithMorpho.address, - parseUnits('10', 8), - ); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: redemptionVaultWithMorpho, owner }, 0); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - await expect( - redemptionVaultWithMorpho['redeemInstant(address,uint256,uint256)']( - stableCoins.usdc.address, - parseUnits('1000'), - 0, - ), - ).to.be.revertedWith('RVM: insufficient shares'); - }); - - it('should fail: Morpho vault has insufficient liquidity during redeemInstant', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - morphoVaultMock, - } = await loadFixture(defaultDeploy); - - // Vault has shares but mock has no liquidity - await morphoVaultMock.mint( - redemptionVaultWithMorpho.address, - parseUnits('10000', 8), - ); - await mintToken(mTBILL, owner, 100000); - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 100000); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: redemptionVaultWithMorpho, owner }, 0); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - // Drain the mock - const mockBalance = await stableCoins.usdc.balanceOf( - morphoVaultMock.address, - ); - await morphoVaultMock.withdrawAdmin( - stableCoins.usdc.address, - ( - await ethers.getSigners() - )[10].address, - mockBalance, - ); - - await expect( - redemptionVaultWithMorpho['redeemInstant(address,uint256,uint256)']( - stableCoins.usdc.address, - parseUnits('1000'), - 0, - ), - ).to.be.revertedWith('MorphoVaultMock: InsufficientLiquidity'); - }); - - // ── Custom recipient tests ─────────────────────────────────────────── - - it('redeem 100 mTBILL (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithMorpho, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when other fn overload is paused (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithMorpho, - encodeFnSelector('redeemInstant(address,uint256,uint256)'), - ); - await mintToken(stableCoins.usdc, redemptionVaultWithMorpho, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when other fn overload is paused', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithMorpho, - encodeFnSelector('redeemInstant(address,uint256,uint256,address)'), - ); - await mintToken(stableCoins.usdc, redemptionVaultWithMorpho, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('should fail: when function with custom recipient is paused', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemInstant(address,uint256,uint256,address)', - ); - await pauseVaultFn(redemptionVaultWithMorpho, selector); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithMorpho.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - }); - - describe('redeemRequest()', () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 0, - { - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithMorpho, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector('redeemRequest(address,uint256)'); - await pauseVaultFn(redemptionVaultWithMorpho, selector); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('redeem request: happy path', async () => { - const { - owner, - redemptionVaultWithMorpho, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithMorpho, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - }); - }); - - describe('redeemFiatRequest()', () => { - it('should fail: when function paused', async () => { - const { - owner, - redemptionVaultWithMorpho, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100000); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithMorpho, - 100000, - ); - const selector = encodeFnSelector('redeemFiatRequest(uint256)'); - await pauseVaultFn(redemptionVaultWithMorpho, selector); - await redeemFiatRequestTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - 100000, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('redeem fiat request: happy path', async () => { - const { owner, redemptionVaultWithMorpho, mTBILL, mTokenToUsdDataFeed } = - await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100000); - await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 100000); - await redeemFiatRequestTest( - { - redemptionVault: redemptionVaultWithMorpho, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - 100000, - ); - }); - }); - - describe('approveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithMorpho: redemptionVault, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await approveRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithMorpho: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - parseUnits('1'), - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithMorpho: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithMorpho: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - }); - }); - - describe('approveRequest() with fiat', async () => { - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithMorpho: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - ); - const requestId = 0; - await changeTokenAllowanceTest( - { vault: redemptionVault, owner }, - constants.AddressZero, - parseUnits('100'), - ); - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - }); - }); - - describe('safeApproveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithMorpho: redemptionVault, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await safeApproveRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithMorpho: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - parseUnits('1'), - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: if new rate greater then variabilityTolerance', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithMorpho: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('6'), - { revertMessage: 'MV: exceed price diviation' }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithMorpho: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.000001'), - ); - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.00001'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('safe approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithMorpho: redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.000001'), - ); - }); - }); - - describe('rejectRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithMorpho: redemptionVault, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await rejectRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithMorpho: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithMorpho: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - ); - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('reject request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithMorpho: redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - ); - }); - }); -}); + await validateImplementation(RedemptionVaultWithMorpho__factory); + }, + { + deployUninitialized: (fixture) => + new RedemptionVaultWithMorphoTest__factory(fixture.owner).deploy(), + initialize: async (fixture, params, opt) => { + await initializeRvWithMorpho( + { ...baseInitParamsRv(fixture), ...params }, + opt?.contract, + opt, + ); + }, + }, + async (defaultDeploy) => { + describe('RedemptionVaultWithMorpho', () => { + describe('setMorphoVault()', () => { + it('should fail: call from address without vault admin role', async () => { + const { + redemptionVaultWithMorpho, + owner, + regularAccounts, + stableCoins, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoVaultTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('should fail: zero token address', async () => { + const { redemptionVaultWithMorpho, owner, morphoVaultMock } = + await loadFixture(defaultDeploy); + + await setMorphoVaultTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + constants.AddressZero, + morphoVaultMock.address, + { + revertCustomError: { + customErrorName: 'InvalidAddress', + }, + }, + ); + }); + + it('should fail: zero vault address', async () => { + const { redemptionVaultWithMorpho, owner, stableCoins } = + await loadFixture(defaultDeploy); + + await setMorphoVaultTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc.address, + constants.AddressZero, + { + revertCustomError: { + customErrorName: 'InvalidAddress', + }, + }, + ); + }); + + it('should fail: asset mismatch', async () => { + const { + redemptionVaultWithMorpho, + owner, + stableCoins, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoVaultTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + stableCoins.dai.address, + morphoVaultMock.address, + { + revertCustomError: { + customErrorName: 'AssetMismatch', + }, + }, + ); + }); + + it('should fail: when function is paused', async () => { + const { + redemptionVaultWithMorpho, + owner, + stableCoins, + morphoVaultMock, + pauseManager, + } = await loadFixture(defaultDeploy); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVaultWithMorpho, + encodeFnSelector('setMorphoVault(address,address)'), + ); + + await setMorphoVaultTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('call from address with vault admin role', async () => { + const { + redemptionVaultWithMorpho, + owner, + stableCoins, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setMorphoVaultTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc.address, + morphoVaultMock.address, + ); + }); + }); + + describe('removeMorphoVault()', () => { + it('should fail: call from address without vault admin role', async () => { + const { + redemptionVaultWithMorpho, + owner, + regularAccounts, + stableCoins, + } = await loadFixture(defaultDeploy); + + await removeMorphoVaultTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc.address, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), + }, + ); + }); + + it('should fail: vault not set', async () => { + const { redemptionVaultWithMorpho, owner, stableCoins } = + await loadFixture(defaultDeploy); + + await removeMorphoVaultTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + stableCoins.dai.address, + { + revertCustomError: { + customErrorName: 'VaultNotSet', + }, + }, + ); + }); + + it('should fail: when function is paused', async () => { + const { + redemptionVaultWithMorpho, + owner, + stableCoins, + pauseManager, + } = await loadFixture(defaultDeploy); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVaultWithMorpho, + encodeFnSelector('removeMorphoVault(address)'), + ); + + await removeMorphoVaultTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc.address, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('call from address with vault admin role', async () => { + const { redemptionVaultWithMorpho, owner, stableCoins } = + await loadFixture(defaultDeploy); + + await removeMorphoVaultTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc.address, + ); + }); + }); + + describe('checkAndRedeemMorpho()', () => { + it('should not redeem when contract has enough balance', async () => { + const { redemptionVaultWithMorpho, stableCoins, morphoVaultMock } = + await loadFixture(defaultDeploy); + + const usdcAmount = parseUnits('1000', 8); + await stableCoins.usdc.mint( + redemptionVaultWithMorpho.address, + usdcAmount, + ); + + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('1000', 8), + ); + + await expect( + redemptionVaultWithMorpho.checkAndRedeemMorpho( + stableCoins.usdc.address, + parseUnits('500', 8), + ), + ).not.reverted; + + expect( + await stableCoins.usdc.balanceOf(redemptionVaultWithMorpho.address), + ).to.be.eq(parseUnits('1000', 8)); + expect( + await morphoVaultMock.balanceOf(redemptionVaultWithMorpho.address), + ).to.be.eq(parseUnits('1000', 8)); + }); + + it('should withdraw missing amount from Morpho', async () => { + const { redemptionVaultWithMorpho, stableCoins, morphoVaultMock } = + await loadFixture(defaultDeploy); + + // Vault has 500 USDC, needs 1000 + const initialUsdc = parseUnits('500', 8); + await stableCoins.usdc.mint( + redemptionVaultWithMorpho.address, + initialUsdc, + ); + + // Vault has 600 Morpho shares (1:1 exchange rate by default) + const sharesAmount = parseUnits('600', 8); + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + sharesAmount, + ); + + await redemptionVaultWithMorpho.checkAndRedeemMorpho( + stableCoins.usdc.address, + parseUnits('1000', 8), + ); + + // Vault should now have 1000 USDC (500 original + 500 withdrawn from Morpho) + const usdcAfter = await stableCoins.usdc.balanceOf( + redemptionVaultWithMorpho.address, + ); + expect(usdcAfter).to.equal(parseUnits('1000', 8)); + + // Share balance should decrease by 500 (1:1 rate) + const sharesAfter = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + expect(sharesAfter).to.equal(parseUnits('100', 8)); + }); + + it('should withdraw missing amount from Morpho when preferLoanLiquidity=true', async () => { + const { + redemptionVaultWithMorpho, + stableCoins, + morphoVaultMock, + owner, + } = await loadFixture(defaultDeploy); + + // Vault has 500 USDC, needs 1000 + const initialUsdc = parseUnits('500', 8); + await stableCoins.usdc.mint( + redemptionVaultWithMorpho.address, + initialUsdc, + ); + + // Vault has 600 Morpho shares (1:1 exchange rate by default) + const sharesAmount = parseUnits('600', 8); + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + sharesAmount, + ); + + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + true, + ); + + await redemptionVaultWithMorpho.checkAndRedeemMorpho( + stableCoins.usdc.address, + parseUnits('1000', 8), + ); + + // Vault should now have 1000 USDC (500 original + 500 withdrawn from Morpho) + const usdcAfter = await stableCoins.usdc.balanceOf( + redemptionVaultWithMorpho.address, + ); + expect(usdcAfter).to.equal(parseUnits('1000', 8)); + + // Share balance should decrease by 500 (1:1 rate) + const sharesAfter = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + expect(sharesAfter).to.equal(parseUnits('100', 8)); + }); + + it('should revert when Morpho vault has insufficient underlying liquidity', async () => { + const { redemptionVaultWithMorpho, stableCoins, morphoVaultMock } = + await loadFixture(defaultDeploy); + + // Vault needs to withdraw from Morpho + await stableCoins.usdc.mint( + redemptionVaultWithMorpho.address, + parseUnits('200', 8), + ); + + // Vault has enough shares + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('1000', 8), + ); + + // Drain the mock's USDC + const mockBalance = await stableCoins.usdc.balanceOf( + morphoVaultMock.address, + ); + await morphoVaultMock.withdrawAdmin( + stableCoins.usdc.address, + ( + await ethers.getSigners() + )[10].address, + mockBalance, + ); + + await expect( + redemptionVaultWithMorpho.checkAndRedeemMorpho( + stableCoins.usdc.address, + parseUnits('1000', 8), + ), + ).to.be.revertedWith('MorphoVaultMock: InsufficientLiquidity'); + }); + + it('should withdraw correctly with non-1:1 exchange rate (shares worth more)', async () => { + const { redemptionVaultWithMorpho, stableCoins, morphoVaultMock } = + await loadFixture(defaultDeploy); + + // Set exchange rate: 1 share = 1.05 underlying (5% interest accrued) + await morphoVaultMock.setExchangeRate(parseUnits('1.05', 18)); + + // Vault has 200 USDC, needs 1000 → missing 800 + await stableCoins.usdc.mint( + redemptionVaultWithMorpho.address, + parseUnits('200', 8), + ); + + // At 1.05 rate, 800 assets needs ceil(800 / 1.05) ≈ 762 shares + // Mint 800 shares (more than enough) + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('800', 8), + ); + + const sharesBefore = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + + await redemptionVaultWithMorpho.checkAndRedeemMorpho( + stableCoins.usdc.address, + parseUnits('1000', 8), + ); + + const usdcAfter = await stableCoins.usdc.balanceOf( + redemptionVaultWithMorpho.address, + ); + expect(usdcAfter).to.equal(parseUnits('1000', 8)); + + const sharesAfter = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + // Shares burned should be less than 800 because each share is worth 1.05 + expect(sharesAfter).to.be.gt(0); + const sharesBurned = sharesBefore.sub(sharesAfter); + expect(sharesBurned).to.be.lt(parseUnits('800', 8)); + }); + + it('shouldnt revert with insufficient shares at non-1:1 exchange rate', async () => { + const { redemptionVaultWithMorpho, stableCoins, morphoVaultMock } = + await loadFixture(defaultDeploy); + + // Set exchange rate: 1 share = 0.95 underlying (loss scenario) + await morphoVaultMock.setExchangeRate(parseUnits('0.95', 18)); + + // Vault has 200 USDC, needs 1000 → missing 800 + await stableCoins.usdc.mint( + redemptionVaultWithMorpho.address, + parseUnits('200', 8), + ); + + // At 0.95 rate, 800 assets needs ceil(800 / 0.95) ≈ 843 shares + // Mint only 800 shares (not enough at this rate) + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('800', 8), + ); + + await expect( + redemptionVaultWithMorpho.checkAndRedeemMorpho( + stableCoins.usdc.address, + parseUnits('1000', 8), + ), + ).to.not.be.reverted; + }); + }); + + describe('redeemInstant()', () => { + describe('preferLoanLiquidity=true', () => { + it('redeem 100 mTBILL when vault has enough USDC (no Morpho needed)', async () => { + const { + owner, + redemptionVaultWithMorpho, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await mintToken( + stableCoins.usdc, + redemptionVaultWithMorpho, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 100); + await addPaymentTokenTest( + { vault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + true, + ); + + const sharesBefore = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.usdc, + 100, + ); + + const sharesAfter = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + expect(sharesAfter).to.equal(sharesBefore); + }); + + it('when vault has no USDC but has Morpho shares and LP liquidity, LP liquidity should be used first', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithMorpho, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + morphoVaultMock, + loanLp, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadFixture(defaultDeploy); + + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('9900', 8), + ); + await mintToken( + stableCoins.usdc, + redemptionVaultLoanSwapper, + 100000, + ); + await mintToken(mTokenLoan, loanLp, 100000); + await approveBase18( + loanLp, + mTokenLoan, + redemptionVaultWithMorpho, + 100000, + ); + + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest( + { vault: redemptionVaultWithMorpho, owner }, + 0, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + true, + ); + + const sharesBefore = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + const loanLpBalanceBefore = await mTokenLoan.balanceOf( + loanLp.address, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + }, + }, + stableCoins.usdc, + 1000, + ); + + const sharesAfter = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + const loanLpBalanceAfter = await mTokenLoan.balanceOf( + loanLp.address, + ); + + expect(sharesAfter).to.equal(sharesBefore); + expect(loanLpBalanceAfter).to.be.lt(loanLpBalanceBefore); + }); + + it('redeem 1000 mTBILL when vault has no USDC but has Morpho shares', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithMorpho, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('9900', 8), + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest( + { vault: redemptionVaultWithMorpho, owner }, + 0, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + true, + ); + + const sharesBefore = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + }, + }, + stableCoins.usdc, + 1000, + ); + + const sharesAfter = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + expect(sharesAfter).to.be.lt(sharesBefore); + }); + + it('when vault partially has USDC, partially has Morpho shares and partially has LP liquidity, so all the liquidity should be used', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithMorpho, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + morphoVaultMock, + loanLp, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadFixture(defaultDeploy); + + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('300', 8), + ); + await mintToken(stableCoins.usdc, redemptionVaultLoanSwapper, 300); + await mintToken(stableCoins.usdc, redemptionVaultWithMorpho, 400); + await mintToken(mTokenLoan, loanLp, 300); + await approveBase18( + loanLp, + mTokenLoan, + redemptionVaultWithMorpho, + 300, + ); + + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest( + { vault: redemptionVaultWithMorpho, owner }, + 0, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + true, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + }, + }, + stableCoins.usdc, + 1000, + ); + + const sharesAfter = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + expect(sharesAfter).to.be.eq(0); + expect( + await stableCoins.usdc.balanceOf( + redemptionVaultWithMorpho.address, + ), + ).to.be.eq(0); + expect( + await stableCoins.usdc.balanceOf( + redemptionVaultLoanSwapper.address, + ), + ).to.be.eq(0); + expect(await mTokenLoan.balanceOf(loanLp.address)).to.be.eq(0); + }); + }); + + it('redeem 100 mTBILL when vault has enough USDC (no Morpho needed)', async () => { + const { + owner, + redemptionVaultWithMorpho, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await mintToken(stableCoins.usdc, redemptionVaultWithMorpho, 100000); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 100); + await addPaymentTokenTest( + { vault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + const sharesBefore = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.usdc, + 100, + ); + + // Share balance should not change + const sharesAfter = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + expect(sharesAfter).to.equal(sharesBefore); + }); + + it('redeem 1000 mTBILL when vault has no USDC but has Morpho shares', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithMorpho, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + // Mint shares to vault (enough for redemption at 1:1 rate) + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('9900', 8), + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest( + { vault: redemptionVaultWithMorpho, owner }, + 0, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + const sharesBefore = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + }, + }, + stableCoins.usdc, + 1000, + ); + + const sharesAfter = await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + + // Shares should decrease + expect(sharesAfter).to.be.lt(sharesBefore); + }); + + it('redeem 1000 mTBILL when vault has 100 USDC and sufficient shares (partial Morpho)', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithMorpho, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + // Vault has 100 USDC + shares + await mintToken(stableCoins.usdc, redemptionVaultWithMorpho, 100); + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('9900', 8), + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + }, + }, + stableCoins.usdc, + 1000, + ); + }); + + it('redeem 1000 mTBILL when vault has 100 USDC and insufficient shares (partial Morpho, partial loan lp)', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithMorpho, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + morphoVaultMock, + redemptionVaultLoanSwapper, + loanLp, + mTokenLoan, + } = await loadFixture(defaultDeploy); + + await mintToken(mTokenLoan, loanLp, 200); + await approveBase18( + loanLp, + mTokenLoan, + redemptionVaultWithMorpho, + 200, + ); + await mintToken(stableCoins.usdc, redemptionVaultLoanSwapper, 200); + + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + // Vault has 100 USDC + shares + await mintToken(stableCoins.usdc, redemptionVaultWithMorpho, 100); + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('700', 8), + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + }, + }, + stableCoins.usdc, + 1000, + ); + }); + + it('redeem 1000 mTBILL with different prices (stable 1.03$, mToken 5$) and partial Morpho', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithMorpho, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await mintToken(stableCoins.usdc, redemptionVaultWithMorpho, 100); + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('15000', 8), + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await redemptionVaultWithMorpho.freeFromMinAmount( + owner.address, + true, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + }, + }, + stableCoins.usdc, + 1000, + ); + }); + + it('redeem 1000 mTBILL with waived fee and Morpho withdrawal', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithMorpho, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await mintToken(stableCoins.usdc, redemptionVaultWithMorpho, 100); + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('15000', 8), + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await setWaivedFeeAccountTest( + { vault: redemptionVaultWithMorpho, owner }, + owner.address, + true, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMorpho, + owner, + mTBILL, + mTokenToUsdDataFeed, + waivedFee: true, + additionalLiquidity: async () => { + return await morphoVaultMock.balanceOf( + redemptionVaultWithMorpho.address, + ); + }, + }, + stableCoins.usdc, + 1000, + ); + }); + + it('should fail: insufficient shares so it fallback to loan lp flow', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithMorpho, + stableCoins, + mTBILL, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + await setLoanLpTest( + { redemptionVault: redemptionVaultWithMorpho, owner }, + ethers.constants.AddressZero, + ); + // Vault has no USDC and only 10 shares (not enough for 1000 mTBILL redemption) + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('10', 8), + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest( + { vault: redemptionVaultWithMorpho, owner }, + 0, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + await expect( + redemptionVaultWithMorpho['redeemInstant(address,uint256,uint256)']( + stableCoins.usdc.address, + parseUnits('1000'), + 0, + ), + ).to.be.revertedWith('ERC20: transfer amount exceeds balance'); + }); + + it('should fail: Morpho vault has insufficient liquidity during redeemInstant', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithMorpho, + stableCoins, + mTBILL, + dataFeed, + morphoVaultMock, + } = await loadFixture(defaultDeploy); + + // Vault has shares but mock has no liquidity + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('10000', 8), + ); + await mintToken(mTBILL, owner, 100000); + await approveBase18(owner, mTBILL, redemptionVaultWithMorpho, 100000); + await addPaymentTokenTest( + { vault: redemptionVaultWithMorpho, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest( + { vault: redemptionVaultWithMorpho, owner }, + 0, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + // Drain the mock + const mockBalance = await stableCoins.usdc.balanceOf( + morphoVaultMock.address, + ); + await morphoVaultMock.withdrawAdmin( + stableCoins.usdc.address, + ( + await ethers.getSigners() + )[10].address, + mockBalance, + ); + + await expect( + redemptionVaultWithMorpho['redeemInstant(address,uint256,uint256)']( + stableCoins.usdc.address, + parseUnits('1000'), + 0, + ), + ).to.be.revertedWith('ERC20: transfer amount exceeds balance'); + }); + }); + }); + }, +); diff --git a/test/unit/RedemptionVaultWithSwapper.test.ts b/test/unit/RedemptionVaultWithSwapper.test.ts deleted file mode 100644 index f284be69..00000000 --- a/test/unit/RedemptionVaultWithSwapper.test.ts +++ /dev/null @@ -1,1475 +0,0 @@ -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; -import { expect } from 'chai'; -import { constants } from 'ethers'; -import { parseUnits } from 'ethers/lib/utils'; - -import { encodeFnSelector } from '../../helpers/utils'; -import { RedemptionVaultWithSwapperTest__factory } from '../../typechain-types'; -import { acErrors, blackList, greenList } from '../common/ac.helpers'; -import { - approveBase18, - mintToken, - pauseVaultFn, -} from '../common/common.helpers'; -import { setRoundData } from '../common/data-feed.helpers'; -import { defaultDeploy } from '../common/fixtures'; -import { - addPaymentTokenTest, - changeTokenAllowanceTest, - setInstantFeeTest, - setInstantDailyLimitTest, - setMinAmountTest, - changeTokenFeeTest, -} from '../common/manageable-vault.helpers'; -import { - redeemInstantWithSwapperTest, - setLiquidityProviderTest, - setSwapperVaultTest, -} from '../common/redemption-vault-swapper.helpers'; -import { sanctionUser } from '../common/with-sanctions-list.helpers'; - -describe('MBasisRedemptionVaultWithSwapper', () => { - describe('deployment', () => { - it('should fail: cal; initialize() when already initialized', async () => { - const { redemptionVaultWithSwapper } = await loadFixture(defaultDeploy); - - await expect( - redemptionVaultWithSwapper[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address,address)' - ]( - constants.AddressZero, - { - mToken: constants.AddressZero, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: constants.AddressZero, - tokensReceiver: constants.AddressZero, - }, - { - instantFee: 0, - instantDailyLimit: 0, - }, - constants.AddressZero, - 0, - 0, - { - fiatAdditionalFee: 0, - fiatFlatFee: 0, - minFiatRedeemAmount: 0, - }, - constants.AddressZero, - constants.AddressZero, - constants.AddressZero, - ), - ).revertedWith('Initializable: contract is already initialized'); - }); - - it('should fail: incorrect initialize parameters', async () => { - const { - accessControl, - tokensReceiver, - feeReceiver, - mBASIS, - mBasisToUsdDataFeed, - mockedSanctionsList, - owner, - requestRedeemer, - liquidityProvider, - redemptionVault, - } = await loadFixture(defaultDeploy); - - const redemptionVaultWithSwapper = - await new RedemptionVaultWithSwapperTest__factory(owner).deploy(); - - await expect( - redemptionVaultWithSwapper[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address,address)' - ]( - accessControl.address, - { - mToken: mBASIS.address, - mTokenDataFeed: mBasisToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, - }, - requestRedeemer.address, - constants.AddressZero, - liquidityProvider.address, - ), - ).to.be.reverted; - - await expect( - redemptionVaultWithSwapper[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address,address)' - ]( - accessControl.address, - { - mToken: mBASIS.address, - mTokenDataFeed: mBasisToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, - }, - requestRedeemer.address, - redemptionVault.address, - constants.AddressZero, - ), - ).to.be.reverted; - }); - }); - - describe('setLiquidityProvider()', () => { - it('should fail: call from address without M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithSwapper, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setLiquidityProviderTest( - { vault: redemptionVaultWithSwapper, owner }, - constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: if provider address zero', async () => { - const { redemptionVaultWithSwapper, owner } = await loadFixture( - defaultDeploy, - ); - await setLiquidityProviderTest( - { vault: redemptionVaultWithSwapper, owner }, - constants.AddressZero, - { revertMessage: 'zero address' }, - ); - }); - - it('should fail: if provider address equal current provider address', async () => { - const { redemptionVaultWithSwapper, liquidityProvider, owner } = - await loadFixture(defaultDeploy); - await setLiquidityProviderTest( - { vault: redemptionVaultWithSwapper, owner }, - liquidityProvider.address, - { revertMessage: 'MRVS: already provider' }, - ); - }); - - it('call from address with M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithSwapper, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setLiquidityProviderTest( - { vault: redemptionVaultWithSwapper, owner }, - regularAccounts[0].address, - ); - }); - }); - - describe('setSwapperVault()', () => { - it('should fail: call from address without M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithSwapper, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setSwapperVaultTest( - { vault: redemptionVaultWithSwapper, owner }, - constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: if provider address zero', async () => { - const { redemptionVaultWithSwapper, owner } = await loadFixture( - defaultDeploy, - ); - await setSwapperVaultTest( - { vault: redemptionVaultWithSwapper, owner }, - constants.AddressZero, - { revertMessage: 'zero address' }, - ); - }); - - it('should fail: if provider address equal current provider address', async () => { - const { redemptionVaultWithSwapper, redemptionVault, owner } = - await loadFixture(defaultDeploy); - await setSwapperVaultTest( - { vault: redemptionVaultWithSwapper, owner }, - redemptionVault.address, - { revertMessage: 'MRVS: already provider' }, - ); - }); - - it('call from address with M_BASIS_REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithSwapper, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setSwapperVaultTest( - { vault: redemptionVaultWithSwapper, owner }, - regularAccounts[0].address, - ); - }); - }); - describe('redeemInstant()', () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 0, - { - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - dataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await mintToken(mBASIS, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithSwapper, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemInstant(address,uint256,uint256)', - ); - await pauseVaultFn(redemptionVaultWithSwapper, selector); - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mBASIS, owner, 100); - - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, mBASIS, redemptionVaultWithSwapper, 15); - - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - - await mintToken(stableCoins.dai, redemptionVaultWithSwapper, 100); - await mintToken(mBASIS, owner, 10); - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 15, - { - revertMessage: 'ERC20: burn amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - dataFeed, - mockedAggregator, - mockedAggregatorMToken, - mockedAggregatorMBasis, - } = await loadFixture(defaultDeploy); - - await mintToken(mBASIS, owner, 100_000); - await approveBase18(owner, mBASIS, redemptionVaultWithSwapper, 100_000); - - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 0); - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 10, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 0); - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 10, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMBasis }, 0); - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 10, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: if min receive amount greater then actual', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - dataFeed, - mockedAggregator, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mBASIS, owner, 100_000); - await approveBase18(owner, mBASIS, redemptionVaultWithSwapper, 100_000); - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - minAmount: parseUnits('10000'), - }, - stableCoins.dai, - 999, - { - revertMessage: 'RVS: minReceiveAmount > actual', - }, - ); - }); - - it('should fail: call for amount < minAmount', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - dataFeed, - mockedAggregator, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mBASIS, owner, 100_000); - await approveBase18(owner, mBASIS, redemptionVaultWithSwapper, 100_000); - - await setMinAmountTest( - { vault: redemptionVaultWithSwapper, owner }, - 100_000, - ); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'RV: amount < min', - }, - ); - }); - - it('should fail: if exceed allowance of deposit by token', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - dataFeed, - mockedAggregator, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mBASIS, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVaultWithSwapper, 1_000_000); - await approveBase18(owner, mBASIS, redemptionVaultWithSwapper, 100_000); - - await changeTokenAllowanceTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai.address, - 100, - ); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - - it('should fail: if redeem daily limit exceeded', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - dataFeed, - mockedAggregator, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mBASIS, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVaultWithSwapper, 1_000_000); - await approveBase18(owner, mBASIS, redemptionVaultWithSwapper, 100_000); - - await setInstantDailyLimitTest( - { vault: redemptionVaultWithSwapper, owner }, - 1000, - ); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'MV: exceed limit', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - dataFeed, - mockedAggregator, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 10000, - true, - ); - - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mBASIS, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVaultWithSwapper, 1_000_000); - await approveBase18(owner, mBASIS, redemptionVaultWithSwapper, 100_000); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'RV: amountMTokenIn < fee', - }, - ); - changeTokenFeeTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai.address, - 0, - ); - await setInstantFeeTest( - { vault: redemptionVaultWithSwapper, owner }, - 10000, - ); - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'RV: amountMTokenIn < fee', - }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist ', async () => { - const { - owner, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - redemptionVaultWithSwapper, - mBASIS, - mBasisToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithSwapper.setGreenlistEnable(true); - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist ', async () => { - const { - owner, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - redemptionVaultWithSwapper, - mBASIS, - mBasisToUsdDataFeed, - regularAccounts, - blackListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - redemptionVaultWithSwapper, - mBASIS, - mBasisToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: when function with custom recipient is paused', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - mBasisToUsdDataFeed, - mBASIS, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithSwapper, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemInstant(address,uint256,uint256,address)', - ); - await pauseVaultFn(redemptionVaultWithSwapper, selector); - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - mBASIS, - mBasisToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - mBasisToUsdDataFeed, - mBASIS, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithSwapper.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - mBasisToUsdDataFeed, - mBASIS, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - mBasisToUsdDataFeed, - mBASIS, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - mBasisToUsdDataFeed, - mBASIS, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - mBasisToUsdDataFeed, - mBASIS, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - mBasisToUsdDataFeed, - mBASIS, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: user try to instant redeem fiat', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mBASIS, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVaultWithSwapper, 1_000_000); - await approveBase18(owner, mBASIS, redemptionVaultWithSwapper, 100_000); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - await redemptionVaultWithSwapper.MANUAL_FULLFILMENT_TOKEN(), - 99_999, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: liquidity provider do not have mTBILL to swap', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - dataFeed, - redemptionVault, - liquidityProvider, - } = await loadFixture(defaultDeploy); - - await mintToken(mBASIS, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVaultWithSwapper, 10); - await approveBase18(owner, mBASIS, redemptionVaultWithSwapper, 100_000); - await approveBase18( - liquidityProvider, - mTBILL, - redemptionVaultWithSwapper, - 100_000, - ); - - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 200, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('redeem 100 mBASIS, when contract have enough DAI and all fees are 0', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - dataFeed, - mockedAggregator, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mBASIS, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVaultWithSwapper, 1_000_000); - await approveBase18(owner, mBASIS, redemptionVaultWithSwapper, 100_000); - - await setInstantFeeTest( - { - vault: redemptionVaultWithSwapper, - owner, - }, - 0, - ); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem 100 mBASIS, when contract have enough DAI', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - dataFeed, - mockedAggregator, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mBASIS, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVaultWithSwapper, 1_000_000); - await approveBase18(owner, mBASIS, redemptionVaultWithSwapper, 100_000); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem 100 mBASIS, when contract do not have enough DAI and need to use mTBILL vault', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mBASIS, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - dataFeed, - redemptionVault, - liquidityProvider, - } = await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - - await mintToken(mBASIS, owner, 100_000); - await mintToken(mTBILL, liquidityProvider, 100_000); - await mintToken(stableCoins.dai, redemptionVaultWithSwapper, 10); - await mintToken(stableCoins.dai, redemptionVault, 1_000_000); - await approveBase18(owner, mBASIS, redemptionVaultWithSwapper, 100_000); - await approveBase18( - liquidityProvider, - mTBILL, - redemptionVaultWithSwapper, - 1000000, - ); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mBASIS, - mBasisToUsdDataFeed, - mTokenToUsdDataFeed, - swap: true, - }, - stableCoins.dai, - 100, - ); - }); - }); - - it('redeem 100 mTBILL (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - mBasisToUsdDataFeed, - mBASIS, - mTBILL, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVaultWithSwapper, 100000); - await mintToken(mBASIS, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mBASIS, - redemptionVaultWithSwapper, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - mBasisToUsdDataFeed, - mBASIS, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when recipient == msg.sender (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - mBasisToUsdDataFeed, - mBASIS, - mTBILL, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVaultWithSwapper, 100000); - await mintToken(mBASIS, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mBASIS, - redemptionVaultWithSwapper, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[0], - mBasisToUsdDataFeed, - mBASIS, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when other fn overload is paused (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - mBasisToUsdDataFeed, - mBASIS, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithSwapper, - encodeFnSelector('redeemInstant(address,uint256,uint256)'), - ); - await mintToken(stableCoins.dai, redemptionVaultWithSwapper, 100000); - await mintToken(mBASIS, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mBASIS, - redemptionVaultWithSwapper, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - mBasisToUsdDataFeed, - mBASIS, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when other fn overload is paused', async () => { - const { - owner, - redemptionVaultWithSwapper, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - mBasisToUsdDataFeed, - mBASIS, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithSwapper, - encodeFnSelector('redeemInstant(address,uint256,uint256,address)'), - ); - await mintToken(stableCoins.dai, redemptionVaultWithSwapper, 100000); - await mintToken(mBASIS, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mBASIS, - redemptionVaultWithSwapper, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithSwapper, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemInstantWithSwapperTest( - { - redemptionVaultWithSwapper, - owner, - mTBILL, - mTokenToUsdDataFeed, - mBasisToUsdDataFeed, - mBASIS, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); -}); diff --git a/test/unit/RedemptionVaultWithUSTB.test.ts b/test/unit/RedemptionVaultWithUSTB.test.ts index 9ce5b13d..b1360685 100644 --- a/test/unit/RedemptionVaultWithUSTB.test.ts +++ b/test/unit/RedemptionVaultWithUSTB.test.ts @@ -1,4804 +1,805 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; import { constants } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; -import { ethers } from 'hardhat'; -import { encodeFnSelector } from '../../helpers/utils'; import { - ManageableVaultTester__factory, + baseInitParamsRv, + redemptionVaultSuits, +} from './suits/redemption-vault.suits'; + +import { + RedemptionVaultWithUSTB__factory, RedemptionVaultWithUSTBTest__factory, } from '../../typechain-types'; -import { acErrors, blackList, greenList } from '../common/ac.helpers'; import { approveBase18, + InitializeParamCase, mintToken, - pauseVault, - pauseVaultFn, + validateImplementation, } from '../common/common.helpers'; import { setRoundData } from '../common/data-feed.helpers'; import { defaultDeploy } from '../common/fixtures'; import { addPaymentTokenTest, setInstantFeeTest, - setMinAmountTest, - setInstantDailyLimitTest, - addWaivedFeeAccountTest, - removeWaivedFeeAccountTest, - setVariabilityToleranceTest, - removePaymentTokenTest, - withdrawTest, - changeTokenFeeTest, - changeTokenAllowanceTest, + setWaivedFeeAccountTest, } from '../common/manageable-vault.helpers'; import { - approveRedeemRequestTest, - redeemFiatRequestTest, redeemInstantTest, - redeemRequestTest, - rejectRedeemRequestTest, - safeApproveRedeemRequestTest, - setFiatAdditionalFeeTest, - setMinFiatRedeemAmountTest, + setLoanLpTest, + setPreferLoanLiquidityTest, } from '../common/redemption-vault.helpers'; -import { sanctionUser } from '../common/with-sanctions-list.helpers'; - -describe('RedemptionVaultWithUSTB', function () { - it('deployment', async () => { - const { - redemptionVaultWithUSTB, - ustbRedemption, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - roles, - } = await loadFixture(defaultDeploy); - - expect(await redemptionVaultWithUSTB.mToken()).eq(mTBILL.address); - - expect(await redemptionVaultWithUSTB.ONE_HUNDRED_PERCENT()).eq('10000'); - - expect(await redemptionVaultWithUSTB.paused()).eq(false); - - expect(await redemptionVaultWithUSTB.tokensReceiver()).eq( - tokensReceiver.address, - ); - expect(await redemptionVaultWithUSTB.feeReceiver()).eq(feeReceiver.address); - - expect(await redemptionVaultWithUSTB.minAmount()).eq(1000); - expect(await redemptionVaultWithUSTB.minFiatRedeemAmount()).eq(1000); - - expect(await redemptionVaultWithUSTB.instantFee()).eq('100'); - - expect(await redemptionVaultWithUSTB.instantDailyLimit()).eq( - parseUnits('100000'), - ); - - expect(await redemptionVaultWithUSTB.mTokenDataFeed()).eq( - mTokenToUsdDataFeed.address, - ); - expect(await redemptionVaultWithUSTB.variationTolerance()).eq(1); - - expect(await redemptionVaultWithUSTB.vaultRole()).eq( - roles.tokenRoles.mTBILL.redemptionVaultAdmin, - ); - - expect(await redemptionVaultWithUSTB.MANUAL_FULLFILMENT_TOKEN()).eq( - ethers.constants.AddressZero, - ); +import { + initializeRvWithUstb, + InitializerParamsRvWithUstb, +} from '../common/vault-initializer.helpers'; + +const baseInitParamsRvWithUstb = ( + fixture: Parameters[0], +): InitializerParamsRvWithUstb => ({ + ...baseInitParamsRv(fixture), + ustbRedemption: fixture.ustbRedemption, +}); +const rvWithUstbInitializeParamCases: InitializeParamCase[] = + [ + { + title: 'ustbRedemption is zero address', + params: { ustbRedemption: constants.AddressZero }, + revertCustomError: { + customErrorName: 'InvalidAddress', + args: [constants.AddressZero], + }, + }, + ]; + +redemptionVaultSuits( + 'RedemptionVaultWithUSTB', + defaultDeploy, + { + createNew: async (owner: SignerWithAddress) => + new RedemptionVaultWithUSTBTest__factory(owner).deploy(), + key: 'redemptionVaultWithUSTB', + }, + async (fixture) => { + const { redemptionVaultWithUSTB, ustbRedemption } = fixture; expect(await redemptionVaultWithUSTB.ustbRedemption()).eq( ustbRedemption.address, ); - }); - - it('failing deployment', async () => { - const { - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - accessControl, - mockedSanctionsList, - owner, - } = await loadFixture(defaultDeploy); - - const redemptionVaultWithUSTB = - await new RedemptionVaultWithUSTBTest__factory(owner).deploy(); - - await expect( - redemptionVaultWithUSTB[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address)' - ]( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - parseUnits('100'), - { - fiatAdditionalFee: 10000, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: parseUnits('100'), - }, - constants.AddressZero, - ), - ).to.be.reverted; - }); - - describe('initialization', () => { - it('should fail: call initialize() when already initialized', async () => { - const { redemptionVaultWithUSTB } = await loadFixture(defaultDeploy); - - await expect( - redemptionVaultWithUSTB[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address)' - ]( - constants.AddressZero, - { - mToken: constants.AddressZero, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: constants.AddressZero, - tokensReceiver: constants.AddressZero, - }, - { - instantFee: 0, - instantDailyLimit: 0, - }, - constants.AddressZero, - 0, - 0, - { - fiatAdditionalFee: 0, - fiatFlatFee: 0, - minFiatRedeemAmount: 0, - }, - constants.AddressZero, - constants.AddressZero, - ), - ).revertedWith('Initializable: contract is already initialized'); - }); - - it('should fail: call with initializing == false', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initializeWithoutInitializer( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('Initializable: contract is not initializing'); - }); - - it('should fail: when _tokensReceiver == address(this)', async () => { - const { - owner, - accessControl, - mTBILL, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: vault.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('invalid address'); - }); - - it('should fail: when _feeReceiver == address(this)', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: vault.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('invalid address'); - }); - - it('should fail: when limit = 0', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: 0, - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('zero limit'); - }); - - it('should fail: when mToken dataFeed address zero', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: constants.AddressZero, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - ), - ).revertedWith('zero address'); - }); - - it('should fail: when variationTolarance zero', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mockedSanctionsList, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - const vault = await new ManageableVaultTester__factory(owner).deploy(); - - await expect( - vault.initialize( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 0, - 1000, - ), - ).revertedWith('fee == 0'); - }); - - it('should fail: when ustbRedemption address zero', async () => { - const { - owner, - accessControl, - mTBILL, - tokensReceiver, - feeReceiver, - mTokenToUsdDataFeed, - mockedSanctionsList, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - const redemptionVaultWithUSTB = - await new RedemptionVaultWithUSTBTest__factory(owner).deploy(); - - await expect( - redemptionVaultWithUSTB[ - 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address)' - ]( - accessControl.address, - { - mToken: mTBILL.address, - mTokenDataFeed: mTokenToUsdDataFeed.address, - }, - { - feeReceiver: feeReceiver.address, - tokensReceiver: tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - mockedSanctionsList.address, - 1, - 1000, - { - fiatAdditionalFee: 100, - fiatFlatFee: parseUnits('1'), - minFiatRedeemAmount: 1000, - }, - requestRedeemer.address, - constants.AddressZero, - ), - ).revertedWith('zero address'); - }); - }); - - describe('setMinAmount()', () => { - it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithUSTB, regularAccounts } = - await loadFixture(defaultDeploy); - - await setMinAmountTest({ vault: redemptionVaultWithUSTB, owner }, 1.1, { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, + await validateImplementation(RedemptionVaultWithUSTB__factory); + }, + { + deployUninitialized: (fixture) => + new RedemptionVaultWithUSTBTest__factory(fixture.owner).deploy(), + initialize: async (fixture, params, opt) => { + await initializeRvWithUstb( + { ...baseInitParamsRvWithUstb(fixture), ...params }, + opt?.contract, + opt, + ); + }, + extraParamCases: rvWithUstbInitializeParamCases, + }, + async (defaultDeploy) => { + describe('RedemptionVaultWithUSTB', function () { + describe('redeemInstant()', () => { + describe('preferLoanLiquidity=true', () => { + it('redeem 100 mTBILL when vault has enough USDC (no USTB needed)', async () => { + const { + owner, + redemptionVaultWithUSTB, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + ustbToken, + } = await loadFixture(defaultDeploy); + + await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100000); + await mintToken(ustbToken, redemptionVaultWithUSTB, 9900); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100); + await addPaymentTokenTest( + { vault: redemptionVaultWithUSTB, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithUSTB, owner }, + true, + ); + + const ustbBalanceBefore = await ustbToken.balanceOf( + redemptionVaultWithUSTB.address, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithUSTB, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.usdc, + 100, + ); + + const ustbBalanceAfter = await ustbToken.balanceOf( + redemptionVaultWithUSTB.address, + ); + expect(ustbBalanceAfter).to.equal(ustbBalanceBefore); + }); + + it('when vault has no USDC but has USTB and LP liquidity, LP liquidity should be used first', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithUSTB, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + ustbToken, + ustbRedemption, + loanLp, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadFixture(defaultDeploy); + + await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); + await ustbRedemption.setMaxUstbRedemptionAmount( + parseUnits('2000', 6), + ); + await mintToken(ustbToken, redemptionVaultWithUSTB, 9900); + + await mintToken( + stableCoins.usdc, + redemptionVaultLoanSwapper, + 100000, + ); + await mintToken(mTokenLoan, loanLp, 100000); + await approveBase18( + loanLp, + mTokenLoan, + redemptionVaultWithUSTB, + 100000, + ); + + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithUSTB, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest( + { vault: redemptionVaultWithUSTB, owner }, + 0, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithUSTB, owner }, + true, + ); + + const ustbBalanceBefore = await ustbToken.balanceOf( + redemptionVaultWithUSTB.address, + ); + const loanLpBalanceBefore = await mTokenLoan.balanceOf( + loanLp.address, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithUSTB, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return parseUnits('300', 6); + }, + }, + stableCoins.usdc, + 1000, + ); + + const ustbBalanceAfter = await ustbToken.balanceOf( + redemptionVaultWithUSTB.address, + ); + const loanLpBalanceAfter = await mTokenLoan.balanceOf( + loanLp.address, + ); + + expect(ustbBalanceAfter).to.equal(ustbBalanceBefore); + expect(loanLpBalanceAfter).to.be.lt(loanLpBalanceBefore); + }); + + it('redeem 1000 mTBILL, when vault has no USDC but has USTB', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithUSTB, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + ustbToken, + ustbRedemption, + } = await loadFixture(defaultDeploy); + + await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); + await ustbRedemption.setMaxUstbRedemptionAmount( + parseUnits('2000', 6), + ); + await mintToken(ustbToken, redemptionVaultWithUSTB, 9900); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithUSTB, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest( + { vault: redemptionVaultWithUSTB, owner }, + 0, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithUSTB, owner }, + true, + ); + + const ustbBalanceBefore = await ustbToken.balanceOf( + redemptionVaultWithUSTB.address, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithUSTB, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return ( + await ustbRedemption.calculateUsdcOut( + await ustbToken.balanceOf( + redemptionVaultWithUSTB.address, + ), + ) + ).usdcOutAmountAfterFee; + }, + }, + stableCoins.usdc, + 1000, + ); + + const ustbBalanceAfter = await ustbToken.balanceOf( + redemptionVaultWithUSTB.address, + ); + expect(ustbBalanceAfter).to.be.lt(ustbBalanceBefore); + }); + + it('when vault partially has USDC, partially has USTB and partially has LP liquidity, so all the liquidity should be used', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithUSTB, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + ustbToken, + ustbRedemption, + loanLp, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadFixture(defaultDeploy); + + await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); + await ustbRedemption.setMaxUstbRedemptionAmount( + parseUnits('2000', 6), + ); + await mintToken(ustbToken, redemptionVaultWithUSTB, 9900); + await mintToken(stableCoins.usdc, redemptionVaultLoanSwapper, 300); + await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 400); + await mintToken(mTokenLoan, loanLp, 300); + await approveBase18( + loanLp, + mTokenLoan, + redemptionVaultWithUSTB, + 300, + ); + + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithUSTB, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest( + { vault: redemptionVaultWithUSTB, owner }, + 0, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setPreferLoanLiquidityTest( + { redemptionVault: redemptionVaultWithUSTB, owner }, + true, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithUSTB, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return ( + await ustbRedemption.calculateUsdcOut( + await ustbToken.balanceOf( + redemptionVaultWithUSTB.address, + ), + ) + ).usdcOutAmountAfterFee; + }, + }, + stableCoins.usdc, + 1000, + ); + + expect( + await ustbToken.balanceOf(redemptionVaultWithUSTB.address), + ).to.be.lt(parseUnits('9900', 6)); + expect( + await stableCoins.usdc.balanceOf(redemptionVaultWithUSTB.address), + ).eq(0); + expect( + await stableCoins.usdc.balanceOf( + redemptionVaultLoanSwapper.address, + ), + ).eq(0); + expect(await mTokenLoan.balanceOf(loanLp.address)).eq(0); + }); + }); + + it('should fail: user try to instant redeem more than contract can redeem and it hits loan lp flow', async () => { + const { + owner, + redemptionVaultWithUSTB, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + ustbToken, + } = await loadFixture(defaultDeploy); + + await setLoanLpTest( + { redemptionVault: redemptionVaultWithUSTB, owner }, + constants.AddressZero, + ); + + await mintToken(mTBILL, owner, 100000); + await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100); + await mintToken(ustbToken, redemptionVaultWithUSTB, 100); + + await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100000); + + await addPaymentTokenTest( + { vault: redemptionVaultWithUSTB, owner }, + stableCoins.usdc, + dataFeed.address, + 100, + true, + ); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithUSTB, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.usdc, + 100000, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('redeem 1000 mTBILL, when price of stable is 1$ and mToken price is 1$ and contract does not have USDC, but has 9900 USTB', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithUSTB, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + ustbToken, + ustbRedemption, + } = await loadFixture(defaultDeploy); + + await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); // Set USTB price to $1 + + await mintToken(ustbToken, redemptionVaultWithUSTB, 9900); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithUSTB, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest({ vault: redemptionVaultWithUSTB, owner }, 0); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + const ustbBalanceBefore = await ustbToken.balanceOf( + redemptionVaultWithUSTB.address, + ); + + await ustbRedemption.setMaxUstbRedemptionAmount( + parseUnits('1000', 6), + ); + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithUSTB, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return ( + await ustbRedemption.calculateUsdcOut( + await ustbToken.balanceOf(redemptionVaultWithUSTB.address), + ) + ).usdcOutAmountAfterFee; + }, + }, + stableCoins.usdc, + 1000, + ); + + const ustbBalanceAfter = await ustbToken.balanceOf( + redemptionVaultWithUSTB.address, + ); + + expect(ustbBalanceAfter).to.be.lt(ustbBalanceBefore); + }); + + it('redeem 1000 mTBILL, when price of stable is 1$ and mToken price is 1$ and contract has 100 USDC and 9900 USTB', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithUSTB, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + ustbToken, + ustbRedemption, + } = await loadFixture(defaultDeploy); + + await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); // Set USTB price to $1 + + await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100); + await mintToken(ustbToken, redemptionVaultWithUSTB, 9900); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithUSTB, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + await ustbRedemption.setMaxUstbRedemptionAmount( + parseUnits('1000', 6), + ); + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithUSTB, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return ( + await ustbRedemption.calculateUsdcOut( + await ustbToken.balanceOf(redemptionVaultWithUSTB.address), + ) + ).usdcOutAmountAfterFee; + }, + }, + stableCoins.usdc, + 1000, + ); + }); + + it('redeem 1000 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and contract has 100 USDC and sufficient USTB without checking of minDepositAmount', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithUSTB, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + ustbToken, + ustbRedemption, + } = await loadFixture(defaultDeploy); + + await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); // Set USTB price to $1 + + await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100); + await mintToken(ustbToken, redemptionVaultWithUSTB, 15000); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithUSTB, owner }, + stableCoins.usdc, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await redemptionVaultWithUSTB.freeFromMinAmount(owner.address, true); + + await ustbRedemption.setMaxUstbRedemptionAmount( + parseUnits('6000', 6), + ); + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithUSTB, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity: async () => { + return ( + await ustbRedemption.calculateUsdcOut( + await ustbToken.balanceOf(redemptionVaultWithUSTB.address), + ) + ).usdcOutAmountAfterFee; + }, + }, + stableCoins.usdc, + 1000, + ); + }); + + it('redeem 1000 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and contract has 100 USDC and sufficient USTB and user in waivedFeeRestriction', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVaultWithUSTB, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + ustbToken, + ustbRedemption, + } = await loadFixture(defaultDeploy); + + await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); // Set USTB price to $1 + + await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100); + await mintToken(ustbToken, redemptionVaultWithUSTB, 15000); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 1000); + await addPaymentTokenTest( + { vault: redemptionVaultWithUSTB, owner }, + stableCoins.usdc, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await setWaivedFeeAccountTest( + { vault: redemptionVaultWithUSTB, owner }, + owner.address, + true, + ); + + await ustbRedemption.setMaxUstbRedemptionAmount( + parseUnits('6000', 6), + ); + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithUSTB, + owner, + mTBILL, + mTokenToUsdDataFeed, + waivedFee: true, + additionalLiquidity: async () => { + return ( + await ustbRedemption.calculateUsdcOut( + await ustbToken.balanceOf(redemptionVaultWithUSTB.address), + ) + ).usdcOutAmountAfterFee; + }, + }, + stableCoins.usdc, + 1000, + ); + }); + + it('should fail: when redemption exceeds available USDC in USTB redemption contract', async () => { + const { + owner, + redemptionVaultWithUSTB, + stableCoins, + mTBILL, + dataFeed, + ustbToken, + ustbRedemption, + } = await loadFixture(defaultDeploy); + + await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); // Set USTB price to $1 + + await mintToken(ustbToken, redemptionVaultWithUSTB, 1000000); + await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100); + + const mockBalance = await stableCoins.usdc.balanceOf( + ustbRedemption.address, + ); + await ustbRedemption.withdraw( + stableCoins.usdc.address, + owner.address, + mockBalance, + ); + + expect( + await stableCoins.usdc.balanceOf(ustbRedemption.address), + ).to.equal(0); + + await mintToken(mTBILL, owner, 100000); + await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100000); + await addPaymentTokenTest( + { vault: redemptionVaultWithUSTB, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + // Try to redeem - should fail: because USTB redemption has no USDC + await expect( + redemptionVaultWithUSTB['redeemInstant(address,uint256,uint256)']( + stableCoins.usdc.address, + parseUnits('10000'), + 0, + ), + ).to.be.revertedWith('ERC20: transfer amount exceeds balance'); + }); }); - }); - - it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithUSTB } = await loadFixture( - defaultDeploy, - ); - await setMinAmountTest({ vault: redemptionVaultWithUSTB, owner }, 1.1); - }); - }); - - describe('setMinFiatRedeemAmount()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithUSTB, regularAccounts } = - await loadFixture(defaultDeploy); - - await setMinFiatRedeemAmountTest( - { redemptionVault: redemptionVaultWithUSTB, owner }, - 1.1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithUSTB } = await loadFixture( - defaultDeploy, - ); - await setMinFiatRedeemAmountTest( - { redemptionVault: redemptionVaultWithUSTB, owner }, - 1.1, - ); - }); - }); - - describe('setFiatAdditionalFee()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithUSTB, regularAccounts } = - await loadFixture(defaultDeploy); - - await setFiatAdditionalFeeTest( - { redemptionVault: redemptionVaultWithUSTB, owner }, - 100, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithUSTB } = await loadFixture( - defaultDeploy, - ); - await setFiatAdditionalFeeTest( - { redemptionVault: redemptionVaultWithUSTB, owner }, - 100, - ); - }); - }); - - describe('setInstantDailyLimit()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithUSTB, regularAccounts } = - await loadFixture(defaultDeploy); - - await setInstantDailyLimitTest( - { vault: redemptionVaultWithUSTB, owner }, - parseUnits('1000'), - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, - }, - ); - }); - - it('should fail: try to set 0 limit', async () => { - const { owner, redemptionVaultWithUSTB } = await loadFixture( - defaultDeploy, - ); - - await setInstantDailyLimitTest( - { vault: redemptionVaultWithUSTB, owner }, - constants.Zero, - { - revertMessage: 'MV: limit zero', - }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { owner, redemptionVaultWithUSTB } = await loadFixture( - defaultDeploy, - ); - await setInstantDailyLimitTest( - { vault: redemptionVaultWithUSTB, owner }, - parseUnits('1000'), - ); - }); - }); - - describe('addPaymentToken()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - ethers.constants.AddressZero, - ethers.constants.AddressZero, - 0, - true, - constants.MaxUint256, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when token is already added', async () => { - const { redemptionVaultWithUSTB, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - constants.MaxUint256, - { - revertMessage: 'MV: already added', - }, - ); - }); - - it('should fail: when token dataFeed address zero', async () => { - const { redemptionVaultWithUSTB, stableCoins, owner } = await loadFixture( - defaultDeploy, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - constants.AddressZero, - 0, - true, - constants.MaxUint256, - { - revertMessage: 'zero address', - }, - ); - }); - - it('call when allowance is zero', async () => { - const { redemptionVaultWithUSTB, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - constants.Zero, - ); - }); - - it('call when allowance is not uint256 max', async () => { - const { redemptionVaultWithUSTB, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - false, - parseUnits('100'), - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role and add 3 options on a row', async () => { - const { redemptionVaultWithUSTB, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - }); - }); - - describe('addWaivedFeeAccount()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithUSTB, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if account fee already waived', async () => { - const { redemptionVaultWithUSTB, owner } = await loadFixture( - defaultDeploy, - ); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithUSTB, owner }, - owner.address, - ); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithUSTB, owner }, - owner.address, - { revertMessage: 'MV: already added' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, owner } = await loadFixture( - defaultDeploy, - ); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithUSTB, owner }, - owner.address, - ); - }); - }); - - describe('removeWaivedFeeAccount()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await removeWaivedFeeAccountTest( - { vault: redemptionVaultWithUSTB, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if account not found in restriction', async () => { - const { redemptionVaultWithUSTB, owner } = await loadFixture( - defaultDeploy, - ); - await removeWaivedFeeAccountTest( - { vault: redemptionVaultWithUSTB, owner }, - owner.address, - { revertMessage: 'MV: not found' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, owner } = await loadFixture( - defaultDeploy, - ); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithUSTB, owner }, - owner.address, - ); - await removeWaivedFeeAccountTest( - { vault: redemptionVaultWithUSTB, owner }, - owner.address, - ); - }); - }); - - describe('setFee()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setInstantFeeTest( - { vault: redemptionVaultWithUSTB, owner }, - ethers.constants.Zero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: if new value greater then 100%', async () => { - const { redemptionVaultWithUSTB, owner } = await loadFixture( - defaultDeploy, - ); - await setInstantFeeTest( - { vault: redemptionVaultWithUSTB, owner }, - 10001, - { - revertMessage: 'fee > 100%', - }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, owner } = await loadFixture( - defaultDeploy, - ); - await setInstantFeeTest({ vault: redemptionVaultWithUSTB, owner }, 100); - }); - }); - - describe('setVariabilityTolerance()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await setVariabilityToleranceTest( - { vault: redemptionVaultWithUSTB, owner }, - ethers.constants.Zero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: if new value zero', async () => { - const { redemptionVaultWithUSTB, owner } = await loadFixture( - defaultDeploy, - ); - await setVariabilityToleranceTest( - { vault: redemptionVaultWithUSTB, owner }, - ethers.constants.Zero, - { revertMessage: 'fee == 0' }, - ); - }); - - it('should fail: if new value greater then 100%', async () => { - const { redemptionVaultWithUSTB, owner } = await loadFixture( - defaultDeploy, - ); - await setVariabilityToleranceTest( - { vault: redemptionVaultWithUSTB, owner }, - 10001, - { revertMessage: 'fee > 100%' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, owner } = await loadFixture( - defaultDeploy, - ); - await setVariabilityToleranceTest( - { vault: redemptionVaultWithUSTB, owner }, - 100, - ); - }); - }); - - describe('removePaymentToken()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await removePaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when token is not exists', async () => { - const { owner, redemptionVaultWithUSTB, stableCoins } = await loadFixture( - defaultDeploy, - ); - await removePaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai.address, - { revertMessage: 'MV: not exists' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, stableCoins, owner, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await removePaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai.address, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role and add 3 options on a row', async () => { - const { redemptionVaultWithUSTB, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdt, - dataFeed.address, - 0, - true, - ); - - await removePaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai.address, - ); - await removePaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc.address, - ); - await removePaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdt.address, - ); - - await removePaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdt.address, - { revertMessage: 'MV: not exists' }, - ); - }); - }); - - describe('withdrawToken()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await withdrawTest( - { vault: redemptionVaultWithUSTB, owner }, - ethers.constants.AddressZero, - 0, - ethers.constants.AddressZero, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - - it('should fail: when there is no token in vault', async () => { - const { owner, redemptionVaultWithUSTB, regularAccounts, stableCoins } = - await loadFixture(defaultDeploy); - await withdrawTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - 1, - regularAccounts[0], - { revertMessage: 'ERC20: transfer amount exceeds balance' }, - ); - }); - - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, regularAccounts, stableCoins, owner } = - await loadFixture(defaultDeploy); - await mintToken(stableCoins.dai, redemptionVaultWithUSTB, 1); - await withdrawTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - 1, - regularAccounts[0], - ); - }); - }); - - describe('checkAndRedeemUSTB', () => { - it('should not redeem USTB when contract has enough balance', async () => { - const { redemptionVaultWithUSTB, stableCoins } = await loadFixture( - defaultDeploy, - ); - - const usdcAmount = parseUnits('1000', 8); - await stableCoins.usdc.mint(redemptionVaultWithUSTB.address, usdcAmount); - - const balanceBefore = await stableCoins.usdc.balanceOf( - redemptionVaultWithUSTB.address, - ); - - await redemptionVaultWithUSTB.checkAndRedeemUSTB( - stableCoins.usdc.address, - parseUnits('500', 8), - ); - - const balanceAfter = await stableCoins.usdc.balanceOf( - redemptionVaultWithUSTB.address, - ); - expect(balanceAfter).to.equal(balanceBefore); - }); - - it('should revert when contract has insufficient USTB balance', async () => { - const { redemptionVaultWithUSTB, stableCoins } = await loadFixture( - defaultDeploy, - ); - - // Contract has less USDC than needed - const usdcAmount = parseUnits('500', 8); - await stableCoins.usdc.mint(redemptionVaultWithUSTB.address, usdcAmount); - - // Try to redeem more than available - await expect( - redemptionVaultWithUSTB.checkAndRedeemUSTB( - stableCoins.usdc.address, - parseUnits('1000', 8), - ), - ).to.be.revertedWith('RVU: insufficient USTB balance'); - }); - }); - - describe('freeFromMinAmount()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { redemptionVaultWithUSTB, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithUSTB - .connect(regularAccounts[0]) - .freeFromMinAmount(regularAccounts[1].address, true), - ).to.be.revertedWith('WMAC: hasnt role'); - }); - it('should not fail', async () => { - const { redemptionVaultWithUSTB, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithUSTB.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.not.reverted; - - expect( - await redemptionVaultWithUSTB.isFreeFromMinAmount( - regularAccounts[0].address, - ), - ).to.eq(true); - }); - it('should fail: already in list', async () => { - const { redemptionVaultWithUSTB, regularAccounts } = await loadFixture( - defaultDeploy, - ); - await expect( - redemptionVaultWithUSTB.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.not.reverted; - - expect( - await redemptionVaultWithUSTB.isFreeFromMinAmount( - regularAccounts[0].address, - ), - ).to.eq(true); - - await expect( - redemptionVaultWithUSTB.freeFromMinAmount( - regularAccounts[0].address, - true, - ), - ).to.revertedWith('DV: already free'); - }); - }); - - describe('changeTokenAllowance()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithUSTB, owner }, - ethers.constants.AddressZero, - 0, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: token not exist', async () => { - const { redemptionVaultWithUSTB, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: token not exists' }, - ); - }); - it('should fail: allowance zero', async () => { - const { redemptionVaultWithUSTB, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: zero allowance' }, - ); - }); - it('should fail: if mint exceed allowance', async () => { - const { - redemptionVaultWithUSTB, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, owner, 100000); - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100000); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100000); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc.address, - 100, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai.address, - 100000000, - ); - }); - it('should decrease if allowance < UINT_MAX', async () => { - const { - redemptionVaultWithUSTB, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, owner, 100000); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100000); - - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc.address, - parseUnits('1000'), - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - const tokenConfigBefore = await redemptionVaultWithUSTB.tokensConfig( - stableCoins.usdc.address, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 999, - ); - - const tokenConfigAfter = await redemptionVaultWithUSTB.tokensConfig( - stableCoins.usdc.address, - ); - - expect(tokenConfigBefore.allowance.sub(tokenConfigAfter.allowance)).eq( - parseUnits('999'), - ); + describe('redeemInstant() complex', () => { + it('redeem 100 mtbill, when price is 5$ and contract balance 100 USDC and USTB to cover remaining amount, 125 mtbill when price is 5.1$, 114 mtbill when price is 5.4$', async () => { + const { + owner, + mockedAggregator, + redemptionVaultWithUSTB: redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregatorMToken, + ustbToken, + ustbRedemption, + } = await loadFixture(defaultDeploy); + + await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); // Set USTB price to $1 + + await mintToken(mTBILL, owner, 100 + 125 + 114); + + await mintToken(stableCoins.usdc, redemptionVault, 100); + await mintToken(ustbToken, redemptionVault, 100000); + + await approveBase18(owner, mTBILL, redemptionVault, 100 + 125 + 114); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + await ustbRedemption.setMaxUstbRedemptionAmount( + parseUnits('10000', 6), + ); + + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setRoundData({ mockedAggregator }, 1.04); + + const additionalLiquidity = async () => + ( + await ustbRedemption.calculateUsdcOut( + await ustbToken.balanceOf(redemptionVault.address), + ) + ).usdcOutAmountAfterFee; + + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity, + }, + stableCoins.usdc, + 100, + ); + + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5.1); + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity, + }, + stableCoins.usdc, + 125, + ); + + await setRoundData({ mockedAggregator }, 1.01); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5.4); + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + additionalLiquidity, + }, + stableCoins.usdc, + 114, + ); + }); + }); }); - it('should not decrease if allowance = UINT_MAX', async () => { - const { - redemptionVaultWithUSTB, - stableCoins, - owner, - dataFeed, - mTBILL, - mTokenToUsdDataFeed, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, owner, 100000); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100000); - - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc.address, - constants.MaxUint256, - ); - - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - const tokenConfigBefore = await redemptionVaultWithUSTB.tokensConfig( - stableCoins.usdc.address, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 999, - ); - - const tokenConfigAfter = await redemptionVaultWithUSTB.tokensConfig( - stableCoins.usdc.address, - ); - - expect(tokenConfigBefore.allowance).eq(tokenConfigAfter.allowance); - }); - }); - - describe('changeTokenFee()', () => { - it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, regularAccounts, owner } = - await loadFixture(defaultDeploy); - await changeTokenFeeTest( - { vault: redemptionVaultWithUSTB, owner }, - ethers.constants.AddressZero, - 0, - { revertMessage: acErrors.WMAC_HASNT_ROLE, from: regularAccounts[0] }, - ); - }); - it('should fail: token not exist', async () => { - const { redemptionVaultWithUSTB, owner, stableCoins } = await loadFixture( - defaultDeploy, - ); - await changeTokenFeeTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai.address, - 0, - { revertMessage: 'MV: token not exists' }, - ); - }); - it('should fail: fee > 100%', async () => { - const { redemptionVaultWithUSTB, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai.address, - 10001, - { revertMessage: 'fee > 100%' }, - ); - }); - it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { - const { redemptionVaultWithUSTB, owner, stableCoins, dataFeed } = - await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await changeTokenFeeTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai.address, - 100, - ); - }); - }); - - describe('redeemInstant()', () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 0, - { - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemInstant(address,uint256,uint256)', - ); - await pauseVaultFn(redemptionVaultWithUSTB, selector); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'ERC20: burn amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mockedAggregatorMToken, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.usdc, redemptionVaultWithUSTB, 10); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await mintToken(mTBILL, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 0); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: if min receive amount greater then actual', async () => { - const { - redemptionVaultWithUSTB, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(mTBILL, owner, 100_000); - - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100_000); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - minAmount: parseUnits('1000000'), - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'RVU: minReceiveAmount > actual', - }, - ); - }); - - it('should fail: call for amount < minAmount', async () => { - const { - redemptionVaultWithUSTB, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100_000); - - await setMinAmountTest( - { vault: redemptionVaultWithUSTB, owner }, - 100_000, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'RV: amount < min', - }, - ); - }); - - it('should fail: call when token is invalid', async () => { - const { - redemptionVaultWithUSTB, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - ustbToken, - ustbRedemption, - mockedAggregator, - mockedAggregatorMToken, - } = await loadFixture(defaultDeploy); - - await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 1000); - await mintToken(ustbToken, redemptionVaultWithUSTB, 9900); - - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setInstantFeeTest({ vault: redemptionVaultWithUSTB, owner }, 0); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1000, - { - revertMessage: 'RVU: invalid token', - }, - ); - }); - - it('should fail: if exceed allowance of redeem by token', async () => { - const { - redemptionVaultWithUSTB, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(mTBILL, owner, 100_000); - await changeTokenAllowanceTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc.address, - 100, - ); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100_000); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'MV: exceed allowance', - }, - ); - }); - - it('should fail: if redeem daily limit exceeded', async () => { - const { - redemptionVaultWithUSTB, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 4); - - await mintToken(mTBILL, owner, 100_000); - await setInstantDailyLimitTest( - { vault: redemptionVaultWithUSTB, owner }, - 1000, - ); - - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100_000); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 99_999, - { - revertMessage: 'MV: exceed limit', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 10000, - true, - ); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - revertMessage: 'RV: amountMTokenIn < fee', - }, - ); - - await removePaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: redemptionVaultWithUSTB, owner }, 10000); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { revertMessage: 'RV: amountMTokenIn < fee' }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist ', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithUSTB.setGreenlistEnable(true); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist ', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: when function with custom recipient is paused', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemInstant(address,uint256,uint256,address)', - ); - await pauseVaultFn(redemptionVaultWithUSTB, selector); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithUSTB.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: user try to instant redeem more than contract can redeem', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - ustbToken, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100000); - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100); - await mintToken(ustbToken, redemptionVaultWithUSTB, 100); - - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100000); - - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 100, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100000, - { - revertMessage: 'RVU: insufficient USTB balance', - }, - ); - }); - - it('redeem 100 mTBILL, when price of stable is 1$ and mToken price is 1$ and contract has 100 USDC', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB, - ustbRedemption, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await ustbRedemption.setMaxUstbRedemptionAmount(parseUnits('100', 6)); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - ); - }); - - it('redeem 1000 mTBILL, when price of stable is 1$ and mToken price is 1$ and contract does not have USDC, but has 9900 USTB', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - ustbToken, - ustbRedemption, - } = await loadFixture(defaultDeploy); - - await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); // Set USTB price to $1 - - await mintToken(ustbToken, redemptionVaultWithUSTB, 9900); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setInstantFeeTest({ vault: redemptionVaultWithUSTB, owner }, 0); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - const ustbBalanceBefore = await ustbToken.balanceOf( - redemptionVaultWithUSTB.address, - ); - - await ustbRedemption.setMaxUstbRedemptionAmount(parseUnits('1000', 6)); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1000, - ); - - const ustbBalanceAfter = await ustbToken.balanceOf( - redemptionVaultWithUSTB.address, - ); - - expect(ustbBalanceAfter).to.be.lt(ustbBalanceBefore); - }); - - it('redeem 1000 mTBILL, when price of stable is 1$ and mToken price is 1$ and contract has 100 USDC and 9900 USTB', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - ustbToken, - ustbRedemption, - } = await loadFixture(defaultDeploy); - - await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); // Set USTB price to $1 - - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100); - await mintToken(ustbToken, redemptionVaultWithUSTB, 9900); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - - await ustbRedemption.setMaxUstbRedemptionAmount(parseUnits('1000', 6)); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1000, - ); - }); - - it('redeem 1000 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and contract has 100 USDC and sufficient USTB without checking of minDepositAmount', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - ustbToken, - ustbRedemption, - } = await loadFixture(defaultDeploy); - - await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); // Set USTB price to $1 - - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100); - await mintToken(ustbToken, redemptionVaultWithUSTB, 15000); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redemptionVaultWithUSTB.freeFromMinAmount(owner.address, true); - - await ustbRedemption.setMaxUstbRedemptionAmount(parseUnits('6000', 6)); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 1000, - ); - }); - - it('redeem 1000 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and contract has 100 USDC and sufficient USTB and user in waivedFeeRestriction', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - ustbToken, - ustbRedemption, - } = await loadFixture(defaultDeploy); - - await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); // Set USTB price to $1 - - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100); - await mintToken(ustbToken, redemptionVaultWithUSTB, 15000); - await mintToken(mTBILL, owner, 1000); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 1000); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithUSTB, owner }, - owner.address, - ); - - await ustbRedemption.setMaxUstbRedemptionAmount(parseUnits('6000', 6)); - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - waivedFee: true, - }, - stableCoins.usdc, - 1000, - ); - }); - - it('should fail: when redemption exceeds available USDC in USTB redemption contract', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - ustbToken, - ustbRedemption, - } = await loadFixture(defaultDeploy); - - await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); // Set USTB price to $1 - - await mintToken(ustbToken, redemptionVaultWithUSTB, 1000000); - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100); - - const mockBalance = await stableCoins.usdc.balanceOf( - ustbRedemption.address, - ); - await ustbRedemption.withdraw( - stableCoins.usdc.address, - owner.address, - mockBalance, - ); - - expect(await stableCoins.usdc.balanceOf(ustbRedemption.address)).to.equal( - 0, - ); - - await mintToken(mTBILL, owner, 100000); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100000); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - // Try to redeem - should fail because USTB redemption has no USDC - await expect( - redemptionVaultWithUSTB['redeemInstant(address,uint256,uint256)']( - stableCoins.usdc.address, - parseUnits('10000'), - 0, - ), - ).to.be.revertedWith('USTBRedemptionMock: InsufficientBalance'); - }); - - it('redeem 100 mTBILL (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when recipient == msg.sender (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[0], - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when other fn overload is paused (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithUSTB, - encodeFnSelector('redeemInstant(address,uint256,uint256)'), - ); - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem 100 mTBILL when other fn overload is paused', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithUSTB, - encodeFnSelector('redeemInstant(address,uint256,uint256,address)'), - ); - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - }); - - describe('redeemRequest()', () => { - it('should fail: when there is no token in vault', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'MV: token not exists', - }, - ); - }); - - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 0, - { - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector('redeemRequest(address,uint256)'); - await pauseVaultFn(redemptionVaultWithUSTB, selector); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mockedAggregator, - mockedAggregatorMToken, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await approveBase18(owner, stableCoins.dai, redemptionVaultWithUSTB, 10); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await mintToken(mTBILL, owner, 100_000); - await setRoundData({ mockedAggregator }, 0); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 0); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minAmount', async () => { - const { - redemptionVaultWithUSTB, - mockedAggregator, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100_000); - - await setMinAmountTest( - { vault: redemptionVaultWithUSTB, owner }, - 100_000, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 99_999, - { - revertMessage: 'RV: amount < min', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 10000, - true, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - revertMessage: 'RV: amountMTokenIn < fee', - }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist ', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithUSTB.setGreenlistEnable(true); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist ', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: when function paused (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector( - 'redeemRequest(address,uint256,address)', - ); - await pauseVaultFn(redemptionVaultWithUSTB, selector); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - customRecipient, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithUSTB.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: recipient in blacklist (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - accessControl, - regularAccounts, - customRecipient, - } = await loadFixture(defaultDeploy); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - customRecipient, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: recipient in sanctions list (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - customRecipient, - } = await loadFixture(defaultDeploy); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - customRecipient, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.dai, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('should fail: user try to redeem fiat in basic request (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100); - - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - await redemptionVaultWithUSTB.MANUAL_FULLFILMENT_TOKEN(), - 100, - { - revertMessage: 'RV: tokenOut == fiat', - }, - ); - }); - - it('should fail: user try to redeem fiat in basic request', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100); - - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - await redemptionVaultWithUSTB.MANUAL_FULLFILMENT_TOKEN(), - 100, - { - revertMessage: 'RV: tokenOut == fiat', - }, - ); - }); - - it('redeem request 100 mTBILL, greenlist enabled and user in greenlist ', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - greenListableTester, - mTokenToUsdDataFeed, - accessControl, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVaultWithUSTB.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(stableCoins.dai, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redemptionVaultWithUSTB.freeFromMinAmount(owner.address, true); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVaultWithUSTB, 100); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.dai, - dataFeed.address, - 100, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await addWaivedFeeAccountTest( - { vault: redemptionVaultWithUSTB, owner }, - owner.address, - ); - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - waivedFee: true, - }, - stableCoins.dai, - 100, - ); - }); - - it('redeem request 100 mTBILL (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem request 100 mTBILL when recipient == msg.sender (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient: regularAccounts[0], - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem request 100 mTBILL when other fn overload is paused (custom recipient overload)', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - customRecipient, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithUSTB, - encodeFnSelector('redeemRequest(address,uint256)'), - ); - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - customRecipient, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem request 100 mTBILL when other fn overload is paused', async () => { - const { - owner, - redemptionVaultWithUSTB, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - dataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVaultFn( - redemptionVaultWithUSTB, - encodeFnSelector('redeemRequest(address,uint256,address)'), - ); - await mintToken(stableCoins.usdc, redemptionVaultWithUSTB, 100000); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - mTBILL, - redemptionVaultWithUSTB, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVaultWithUSTB, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { - redemptionVault: redemptionVaultWithUSTB, - owner, - mTBILL, - mTokenToUsdDataFeed, - }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - }, - ); - }); - }); - - describe('redeemFiatRequest()', () => { - it('should fail: when trying to redeem 0 amount', async () => { - const { - owner, - redemptionVaultWithUSTB: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 0, - { - from: regularAccounts[0], - revertMessage: 'RV: invalid amount', - }, - ); - }); - - it('should fail: call with insufficient allowance', async () => { - const { - owner, - redemptionVaultWithUSTB: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - { - revertMessage: 'ERC20: insufficient allowance', - }, - ); - }); - - it('should fail: when function paused', async () => { - const { - owner, - redemptionVaultWithUSTB: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - regularAccounts, - } = await loadFixture(defaultDeploy); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVault, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - const selector = encodeFnSelector('redeemFiatRequest(uint256)'); - await pauseVaultFn(redemptionVault, selector); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: fn paused', - }, - ); - }); - - it('should fail: call with insufficient balance', async () => { - const { - owner, - redemptionVaultWithUSTB: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await approveBase18(owner, mTBILL, redemptionVault, 100); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - - 100, - { - revertMessage: 'ERC20: transfer amount exceeds balance', - }, - ); - }); - - it('should fail: dataFeed rate 0 ', async () => { - const { - owner, - redemptionVaultWithUSTB: redemptionVault, - mTBILL, - mockedAggregatorMToken, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100_000); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 0); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 10, - { - revertMessage: 'DF: feed is deprecated', - }, - ); - }); - - it('should fail: call for amount < minFiatRedeemAmount', async () => { - const { - redemptionVaultWithUSTB: redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100_000); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setMinFiatRedeemAmountTest({ redemptionVault, owner }, 100_000); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 99_999, - { - revertMessage: 'RV: amount < min', - }, - ); - }); - - it('should fail: if some fee = 100%', async () => { - const { - owner, - redemptionVaultWithUSTB: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await setFiatAdditionalFeeTest({ redemptionVault, owner }, 10000); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - { - revertMessage: 'RV: amountMTokenIn < fee', - }, - ); - }); - - it('should fail: greenlist enabled and user not in greenlist ', async () => { - const { - owner, - redemptionVaultWithUSTB: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await redemptionVault.setGreenlistEnable(true); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: user in blacklist ', async () => { - const { - owner, - redemptionVaultWithUSTB: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - blackListableTester, - regularAccounts, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await blackList( - { blacklistable: blackListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - from: regularAccounts[0], - revertMessage: acErrors.WMAC_HAS_ROLE, - }, - ); - }); - - it('should fail: user in sanctions list', async () => { - const { - owner, - redemptionVaultWithUSTB: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - mockedSanctionsList, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await sanctionUser( - { sanctionsList: mockedSanctionsList }, - regularAccounts[0], - ); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - from: regularAccounts[0], - revertMessage: 'WSL: sanctioned', - }, - ); - }); - - it('redeem fiat request 100 mTBILL, greenlist enabled and user in greenlist ', async () => { - const { - owner, - redemptionVaultWithUSTB: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - regularAccounts, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await redemptionVault.setGreenlistEnable(true); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - regularAccounts[0], - ); - - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - { - from: regularAccounts[0], - }, - ); - }); - - it('redeem fiat request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - ); - }); - - it('redeem fiat request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - ); - }); - - it('redeem fiat request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await redemptionVault.freeFromMinAmount(owner.address, true); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - ); - }); - - it('redeem fiat request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await addWaivedFeeAccountTest( - { vault: redemptionVault, owner }, - owner.address, - ); - await redeemFiatRequestTest( - { - redemptionVault, - owner, - mTBILL, - mTokenToUsdDataFeed, - waivedFee: true, - }, - 100, - ); - }); - }); - - describe('approveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithUSTB: redemptionVault, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await approveRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithUSTB: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - parseUnits('1'), - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - }); - }); - - describe('approveRequest() with fiat', async () => { - it('approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB: redemptionVault, - mTBILL, - mTokenToUsdDataFeed, - greenListableTester, - accessControl, - } = await loadFixture(defaultDeploy); - - await greenList( - { greenlistable: greenListableTester, accessControl, owner }, - owner, - ); - - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemFiatRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 100, - ); - const requestId = 0; - await changeTokenAllowanceTest( - { vault: redemptionVault, owner }, - constants.AddressZero, - parseUnits('100'), - ); - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - }); - }); - - describe('safeApproveRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithUSTB: redemptionVault, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await safeApproveRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - parseUnits('1'), - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithUSTB: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - parseUnits('1'), - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: if new rate greater then variabilityTolerance', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('6'), - { revertMessage: 'MV: exceed price diviation' }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.000001'), - ); - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.00001'), - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('safe approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB: redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('5.000001'), - ); - }); - }); - - describe('rejectRequest()', async () => { - it('should fail: call from address without vault admin role', async () => { - const { - redemptionVaultWithUSTB: redemptionVault, - regularAccounts, - mTokenToUsdDataFeed, - mTBILL, - } = await loadFixture(defaultDeploy); - await rejectRedeemRequestTest( - { - redemptionVault, - owner: regularAccounts[1], - mTBILL, - mTokenToUsdDataFeed, - }, - 1, - { - revertMessage: 'WMAC: hasnt role', - }, - ); - }); - - it('should fail: request by id not exist', async () => { - const { - owner, - redemptionVaultWithUSTB: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - 1, - { - revertMessage: 'RV: request not exist', - }, - ); - }); - - it('should fail: request already processed', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1.001); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - ); - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - { revertMessage: 'RV: request not pending' }, - ); - }); - - it('safe approve request from vaut admin account', async () => { - const { - owner, - mockedAggregator, - mockedAggregatorMToken, - redemptionVaultWithUSTB: redemptionVault, - stableCoins, - mTBILL, - mTokenToUsdDataFeed, - dataFeed, - } = await loadFixture(defaultDeploy); - - await mintToken(stableCoins.dai, redemptionVault, 100000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await setRoundData({ mockedAggregator }, 1.03); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - ); - const requestId = 0; - - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - ); - }); - }); - - describe('redeemRequest() complex', () => { - it('should fail: when is paused', async () => { - const { - redemptionVaultWithUSTB: redemptionVault, - owner, - mTBILL, - stableCoins, - regularAccounts, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(redemptionVault); - await mintToken(stableCoins.dai, redemptionVault, 100); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.dai, - redemptionVault, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('is on pause, but admin can use everything', async () => { - const { - redemptionVaultWithUSTB: redemptionVault, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(redemptionVault); - - await mintToken(stableCoins.dai, redemptionVault, 1000); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, stableCoins.dai, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100, - { - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('call for amount == minAmount, then approve', async () => { - const { - redemptionVaultWithUSTB: redemptionVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - requestRedeemer, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100_000, - ); - - const requestId = 0; - - await approveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1'), - ); - }); - - it('call for amount == minAmount, then safe approve', async () => { - const { - redemptionVaultWithUSTB: redemptionVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - requestRedeemer, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await mintToken(stableCoins.dai, requestRedeemer, 100000); - await approveBase18( - requestRedeemer, - stableCoins.dai, - redemptionVault, - 100000, - ); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: redemptionVault, owner }, 10_000); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 10_000, - ); - - const requestId = 0; - - await safeApproveRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - parseUnits('1.000001'), - ); - }); - - it('call for amount == minAmount, then reject', async () => { - const { - redemptionVaultWithUSTB: redemptionVault, - mockedAggregator, - mockedAggregatorMToken, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.dai, - dataFeed.address, - 0, - true, - ); - await setRoundData({ mockedAggregator }, 1); - - await mintToken(mTBILL, owner, 100_000); - await mintToken(stableCoins.dai, redemptionVault, 100_000); - await approveBase18(owner, mTBILL, redemptionVault, 100_000); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); - await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); - - await redeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.dai, - 100_000, - ); - - const requestId = 0; - - await rejectRedeemRequestTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - +requestId, - ); - }); - }); - - describe('redeemInstant() complex', () => { - it('should fail: when is paused', async () => { - const { - redemptionVaultWithUSTB: redemptionVault, - owner, - mTBILL, - stableCoins, - regularAccounts, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(redemptionVault); - await mintToken(stableCoins.usdc, redemptionVault, 100); - await mintToken(mTBILL, regularAccounts[0], 100); - await approveBase18( - regularAccounts[0], - stableCoins.usdc, - redemptionVault, - 100, - ); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 100, - { - from: regularAccounts[0], - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('is on pause, but admin can use everything', async () => { - const { - redemptionVaultWithUSTB: redemptionVault, - owner, - mTBILL, - stableCoins, - dataFeed, - mTokenToUsdDataFeed, - } = await loadFixture(defaultDeploy); - - await pauseVault(redemptionVault); - - await mintToken(stableCoins.usdc, redemptionVault, 100); - await mintToken(mTBILL, owner, 100); - await approveBase18(owner, mTBILL, redemptionVault, 100); - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 100, - { - revertMessage: 'Pausable: paused', - }, - ); - }); - - it('redeem 100 mtbill, when price is 5$ and contract balance 100 USDC and USTB to cover remaining amount, 125 mtbill when price is 5.1$, 114 mtbill when price is 5.4$', async () => { - const { - owner, - mockedAggregator, - redemptionVaultWithUSTB: redemptionVault, - stableCoins, - mTBILL, - dataFeed, - mTokenToUsdDataFeed, - mockedAggregatorMToken, - ustbToken, - ustbRedemption, - } = await loadFixture(defaultDeploy); - - await ustbRedemption.setChainlinkData(parseUnits('1', 8), false); // Set USTB price to $1 - - await mintToken(mTBILL, owner, 100 + 125 + 114); - - await mintToken(stableCoins.usdc, redemptionVault, 100); - await mintToken(ustbToken, redemptionVault, 100000); - - await approveBase18(owner, mTBILL, redemptionVault, 100 + 125 + 114); - - await addPaymentTokenTest( - { vault: redemptionVault, owner }, - stableCoins.usdc, - dataFeed.address, - 0, - true, - ); - - await ustbRedemption.setMaxUstbRedemptionAmount(parseUnits('10000', 6)); - - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); - await setRoundData({ mockedAggregator }, 1.04); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 100, - ); - - await setRoundData({ mockedAggregator }, 1); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5.1); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 125, - ); - - await setRoundData({ mockedAggregator }, 1.01); - await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5.4); - await redeemInstantTest( - { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, - stableCoins.usdc, - 114, - ); - }); - }); -}); + }, +); diff --git a/test/unit/WithSanctionsList.test.ts b/test/unit/WithSanctionsList.test.ts index f25b6e11..344319b1 100644 --- a/test/unit/WithSanctionsList.test.ts +++ b/test/unit/WithSanctionsList.test.ts @@ -2,8 +2,13 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; import { constants } from 'ethers'; +import { encodeFnSelector } from '../../helpers/utils'; import { WithSanctionsListTester__factory } from '../../typechain-types'; -import { acErrors } from '../common/ac.helpers'; +import { + acErrors, + setPermissionRoleTester, + setupGrantOperatorRole, +} from '../common/ac.helpers'; import { defaultDeploy } from '../common/fixtures'; import { sanctionUser, @@ -29,8 +34,7 @@ describe('WithSanctionsList', function () { ).deploy(); await expect( - withSanctionsList.initializeWithoutInitializer( - constants.AddressZero, + withSanctionsList.initializeUnchainedWithoutInitializer( constants.AddressZero, ), ).revertedWith('Initializable: contract is not initializing'); @@ -56,7 +60,7 @@ describe('WithSanctionsList', function () { withSanctionsListTester.onlyNotSanctionedTester( regularAccounts[0].address, ), - ).revertedWith('WSL: sanctioned'); + ).revertedWithCustomError(withSanctionsListTester, 'Sanctioned'); }); it('call from not sanctioned user', async () => { @@ -82,7 +86,7 @@ describe('WithSanctionsList', function () { constants.AddressZero, { from: regularAccounts[0], - revertMessage: acErrors.WMAC_HASNT_ROLE, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION(), }, ); }); @@ -91,7 +95,7 @@ describe('WithSanctionsList', function () { const { accessControl, withSanctionsListTester, owner } = await loadFixture(defaultDeploy); - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( await withSanctionsListTester.sanctionsListAdminRole(), owner.address, ); @@ -101,6 +105,100 @@ describe('WithSanctionsList', function () { constants.AddressZero, ); }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, withSanctionsListTester, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + const sanctionsListAdmin = + await withSanctionsListTester.sanctionsListAdminRole(); + const selector = encodeFnSelector('setSanctionsList(address)'); + + await accessControl['grantRole(bytes32,address)']( + sanctionsListAdmin, + owner.address, + ); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: sanctionsListAdmin, + targetContract: withSanctionsListTester.address, + functionSelector: selector, + grantOperator: owner, + }); + + const user = regularAccounts[0]; + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + withSanctionsListTester.address, + selector, + [ + { + account: user.address, + enabled: true, + }, + ], + ); + expect(await accessControl.hasRole(sanctionsListAdmin, user.address)).eq( + false, + ); + + await setSanctionsList( + { withSanctionsList: withSanctionsListTester, owner }, + constants.AddressZero, + { from: user }, + ); + }); + + it('succeeds with scoped permission and sanctions list admin role', async () => { + const { accessControl, withSanctionsListTester, owner, regularAccounts } = + await loadFixture(defaultDeploy); + + const sanctionsListAdmin = + await withSanctionsListTester.sanctionsListAdminRole(); + const selector = encodeFnSelector('setSanctionsList(address)'); + const user = regularAccounts[0]; + + await accessControl['grantRole(bytes32,address)']( + sanctionsListAdmin, + owner.address, + ); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: sanctionsListAdmin, + targetContract: withSanctionsListTester.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + withSanctionsListTester.address, + selector, + [ + { + account: user.address, + enabled: true, + }, + ], + ); + + await accessControl['grantRole(bytes32,address)']( + sanctionsListAdmin, + user.address, + ); + + await setSanctionsList( + { withSanctionsList: withSanctionsListTester, owner }, + constants.AddressZero, + { from: user }, + ); + }); }); describe('onlyNotSanctionedTester()', () => { @@ -113,7 +211,7 @@ describe('WithSanctionsList', function () { regularAccounts, } = await loadFixture(defaultDeploy); - await accessControl.grantRole( + await accessControl['grantRole(bytes32,address)']( await withSanctionsListTester.sanctionsListAdminRole(), owner.address, ); diff --git a/test/unit/misc/AcreAdapter.test.ts b/test/unit/misc/AcreAdapter.test.ts index 55e4e893..eae77c4f 100644 --- a/test/unit/misc/AcreAdapter.test.ts +++ b/test/unit/misc/AcreAdapter.test.ts @@ -1,25 +1,21 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; -import { constants } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; -import { - AcreAdapter__factory, - DepositVaultTest__factory, -} from '../../../typechain-types'; +import { AcreAdapter__factory } from '../../../typechain-types'; import { acreAdapterFixture } from '../../common/fixtures'; import { addPaymentTokenTest, - addWaivedFeeAccountTest, changeTokenFeeTest, removePaymentTokenTest, - removeWaivedFeeAccountTest, + setWaivedFeeAccountTest, setInstantFeeTest, } from '../../common/manageable-vault.helpers'; import { acreWrapperDepositTest, acreWrapperRequestRedeemTest, } from '../../common/misc/acre.helpers'; +import { initializeDv } from '../../common/vault-initializer.helpers'; describe('AcreAdapter', () => { it('initialize', async () => { @@ -42,30 +38,10 @@ describe('AcreAdapter', () => { fixture.mTokenToUsdDataFeed.address, ); - const dvWithDifferentDataFeed = await new DepositVaultTest__factory( - fixture.owner, - ).deploy(); - - await dvWithDifferentDataFeed.initialize( - fixture.accessControl.address, - { - mToken: fixture.mTBILL.address, - mTokenDataFeed: fixture.dataFeed.address, - }, - { - feeReceiver: fixture.feeReceiver.address, - tokensReceiver: fixture.tokensReceiver.address, - }, - { - instantFee: 100, - instantDailyLimit: parseUnits('100000'), - }, - fixture.mockedSanctionsList.address, - 1, - parseUnits('100'), - 0, - constants.MaxUint256, - ); + const dvWithDifferentDataFeed = await initializeDv({ + ...fixture, + mTokenToUsdDataFeed: fixture.dataFeedGrowth, + }); await expect( fixture.owner.sendTransaction( @@ -81,7 +57,7 @@ describe('AcreAdapter', () => { fixture.owner.sendTransaction( new AcreAdapter__factory().getDeployTransaction( fixture.depositVault.address, - fixture.redemptionVaultWithSwapper.address, + fixture.redemptionVaultLoanSwapper.address, fixture.stableCoins.usdc.address, ), ), @@ -119,9 +95,10 @@ describe('AcreAdapter', () => { 10, ); - await addWaivedFeeAccountTest( + await setWaivedFeeAccountTest( { vault: fixture.depositVault, owner: fixture.owner }, fixture.acreUsdcMTbillAdapter.address, + true, ); await acreWrapperDepositTest(fixture, 100); }); @@ -154,7 +131,10 @@ describe('AcreAdapter', () => { ); await acreWrapperDepositTest(fixture, 100, undefined, { - revertMessage: 'DV: minReceiveAmount > actual', + revertCustomError: { + contract: fixture.depositVault, + customErrorName: 'SlippageExceeded', + }, }); }); @@ -168,7 +148,10 @@ describe('AcreAdapter', () => { ); await acreWrapperDepositTest(fixture, 100, undefined, { - revertMessage: 'DV: minReceiveAmount > actual', + revertCustomError: { + contract: fixture.depositVault, + customErrorName: 'SlippageExceeded', + }, }); }); }); @@ -246,9 +229,10 @@ describe('AcreAdapter', () => { it('should fail: when not fee waived', async () => { const fixture = await loadFixture(acreAdapterFixture); - await removeWaivedFeeAccountTest( + await setWaivedFeeAccountTest( { vault: fixture.redemptionVault, owner: fixture.owner }, fixture.acreUsdcMTbillAdapter.address, + false, ); await acreWrapperRequestRedeemTest(fixture, 20, undefined, { @@ -296,9 +280,10 @@ describe('AcreAdapter', () => { it('should not account any fees', async () => { const fixture = await loadFixture(acreAdapterFixture); - await removeWaivedFeeAccountTest( + await setWaivedFeeAccountTest( { vault: fixture.redemptionVault, owner: fixture.owner }, fixture.acreUsdcMTbillAdapter.address, + false, ); await setInstantFeeTest( diff --git a/test/unit/Axelar.test.ts b/test/unit/misc/Axelar.test.ts similarity index 94% rename from test/unit/Axelar.test.ts rename to test/unit/misc/Axelar.test.ts index 3af62094..6b07a9ac 100644 --- a/test/unit/Axelar.test.ts +++ b/test/unit/misc/Axelar.test.ts @@ -7,17 +7,17 @@ import { constants, ethers } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; import hre from 'hardhat'; -import { MidasAxelarVaultExecutableTester } from '../../typechain-types'; -import { depositAndSend, redeemAndSend } from '../common/axelar.helpers'; -import { approveBase18, mintToken } from '../common/common.helpers'; -import { setRoundData } from '../common/data-feed.helpers'; -import { deployProxyContract } from '../common/deploy.helpers'; -import { axelarFixture } from '../common/fixtures'; +import { MidasAxelarVaultExecutableTester } from '../../../typechain-types'; +import { depositAndSend, redeemAndSend } from '../../common/axelar.helpers'; +import { approveBase18, mintToken } from '../../common/common.helpers'; +import { setRoundData } from '../../common/data-feed.helpers'; +import { deployProxyContract } from '../../common/deploy.helpers'; +import { axelarFixture } from '../../common/fixtures'; import { addPaymentTokenTest, setInstantFeeTest, setMinAmountTest, -} from '../common/manageable-vault.helpers'; +} from '../../common/manageable-vault.helpers'; describe('Axelar', function () { describe('MidasAxelarVaultExecutable', () => { @@ -29,7 +29,6 @@ describe('Axelar', function () { mTBILL, chainNameHashA, axelarItsA, - mTokenToUsdDataFeed, depositVault, redemptionVault, paymentTokenId, @@ -245,7 +244,10 @@ describe('Axelar', function () { minReceiveAmount: parseUnits('101'), }, { - revertMessage: 'DV: minReceiveAmount > actual', + revertCustomError: { + contract: depositVault, + customErrorName: 'SlippageExceeded', + }, }, ); }); @@ -429,7 +431,10 @@ describe('Axelar', function () { minReceiveAmount: parseUnits('1000'), }, { - revertMessage: 'RV: minReceiveAmount > actual', + revertCustomError: { + contract: redemptionVault, + customErrorName: 'SlippageExceeded', + }, }, ); }); @@ -699,20 +704,7 @@ describe('Axelar', function () { ).eq(parseUnits('100', 8)); await expect(executor.redeemPublic(owner.address, parseUnits('50'), 0)) - .emit( - redemptionVault, - redemptionVault.interface.events[ - 'RedeemInstantWithCustomRecipient(address,address,address,uint256,uint256,uint256)' - ].name, - ) - .withArgs( - executor.address, - stableCoins.usdc.address, - owner.address, - parseUnits('50'), - 0, - parseUnits('100'), - ); + .not.reverted; }); }); @@ -905,23 +897,7 @@ describe('Axelar', function () { 0, constants.HashZero, ), - ) - .emit( - depositVault, - depositVault.interface.events[ - 'DepositInstantWithCustomRecipient(address,address,address,uint256,uint256,uint256,uint256,bytes32)' - ].name, - ) - .withArgs( - executor.address, - stableCoins.usdc.address, - owner.address, - parseUnits('100'), - parseUnits('100'), - 0, - parseUnits('50'), - constants.HashZero, - ); + ).not.reverted; }); }); diff --git a/test/unit/BandProtocolAdapter.test.ts b/test/unit/misc/BandProtocolAdapter.test.ts similarity index 97% rename from test/unit/BandProtocolAdapter.test.ts rename to test/unit/misc/BandProtocolAdapter.test.ts index 6c80f334..fa65cce3 100644 --- a/test/unit/BandProtocolAdapter.test.ts +++ b/test/unit/misc/BandProtocolAdapter.test.ts @@ -7,9 +7,9 @@ import { DataFeedToBandStdAdapter__factory, CompositeDataFeedToBandStdAdapter__factory, CompositeDataFeedTest__factory, -} from '../../typechain-types'; -import { setRoundData } from '../common/data-feed.helpers'; -import { defaultDeploy } from '../common/fixtures'; +} from '../../../typechain-types'; +import { setRoundData } from '../../common/data-feed.helpers'; +import { defaultDeploy } from '../../common/fixtures'; describe('DataFeedToBandStdAdapter', function () { const baseSymbol = 'mTBILL'; @@ -165,8 +165,9 @@ describe('DataFeedToBandStdAdapter', function () { expect(referenceData.rate).eq(parseUnits(largePrice)); }); - it('should fail when underlying feed is unhealthy', async () => { - const { owner, dataFeedUnhealthy } = fixture; + it('should fail: when underlying feed is unhealthy', async () => { + const { owner, deployUnhealthyFeed } = fixture; + const { dataFeedUnhealthy } = await deployUnhealthyFeed(); const dataFeedToBandStdAdapter = await new DataFeedToBandStdAdapter__factory(owner).deploy( @@ -180,9 +181,10 @@ describe('DataFeedToBandStdAdapter', function () { ).to.be.reverted; }); - it('should fail when underlying feed is deprecated', async () => { - const { owner, dataFeedDeprecated } = fixture; + it('should fail: when underlying feed is deprecated', async () => { + const { owner, deployDeprecatedFeed } = fixture; + const { dataFeedDeprecated } = await deployDeprecatedFeed(); const dataFeedToBandStdAdapter = await new DataFeedToBandStdAdapter__factory(owner).deploy( dataFeedDeprecated.address, @@ -368,7 +370,7 @@ describe('DataFeedToBandStdAdapter', function () { }); describe('Error handling', () => { - it('should fail with proper error messages for invalid pairs', async () => { + it('should fail: with proper error messages for invalid pairs', async () => { const { dataFeedToBandStdAdapter } = fixture; // Test various invalid combinations @@ -385,7 +387,7 @@ describe('DataFeedToBandStdAdapter', function () { ).revertedWith('DFBSA: unsupported pair'); }); - it('should fail with proper error messages for bulk operations', async () => { + it('should fail: with proper error messages for bulk operations', async () => { const { dataFeedToBandStdAdapter } = fixture; await expect( @@ -638,7 +640,7 @@ describe('CompositeDataFeedToBandStdAdapter', function () { expect(referenceData.rate).eq(parseUnits(largeNumerator)); }); - it('should fail when underlying composite feed is unhealthy', async () => { + it('should fail: when underlying composite feed is unhealthy', async () => { const { owner, compositeDataFeed } = fixture; // Create a new composite feed with unhealthy settings @@ -1006,7 +1008,7 @@ describe('CompositeDataFeedToBandStdAdapter', function () { }); describe('Error handling', () => { - it('should fail with proper error messages for invalid pairs', async () => { + it('should fail: with proper error messages for invalid pairs', async () => { const { compositeDataFeedToBandStdAdapter } = fixture; // Test various invalid combinations @@ -1029,7 +1031,7 @@ describe('CompositeDataFeedToBandStdAdapter', function () { ).revertedWith('DFBSA: unsupported pair'); }); - it('should fail with proper error messages for bulk operations', async () => { + it('should fail: with proper error messages for bulk operations', async () => { const { compositeDataFeedToBandStdAdapter } = fixture; await expect( diff --git a/test/unit/LayerZero.test.ts b/test/unit/misc/LayerZero.test.ts similarity index 96% rename from test/unit/LayerZero.test.ts rename to test/unit/misc/LayerZero.test.ts index a2106b91..0510d7ab 100644 --- a/test/unit/LayerZero.test.ts +++ b/test/unit/misc/LayerZero.test.ts @@ -11,24 +11,25 @@ import hre from 'hardhat'; import { MidasLzOFTAdapter__factory, MidasLzVaultComposerSyncTester, -} from '../../typechain-types'; -import { approveBase18, mintToken } from '../common/common.helpers'; -import { setRoundData } from '../common/data-feed.helpers'; -import { deployProxyContract } from '../common/deploy.helpers'; -import { layerZeroFixture } from '../common/fixtures'; +} from '../../../typechain-types'; +import { acErrors, blackList } from '../../common/ac.helpers'; +import { approveBase18, mintToken } from '../../common/common.helpers'; +import { setRoundData } from '../../common/data-feed.helpers'; +import { deployProxyContract } from '../../common/deploy.helpers'; +import { layerZeroFixture } from '../../common/fixtures'; import { depositAndSend, redeemAndSend, sendOft, sendOftLockBox, setRateLimitConfig, -} from '../common/layerzero.helpers'; +} from '../../common/layerzero.helpers'; import { addPaymentTokenTest, setInstantFeeTest, setMinAmountTest, -} from '../common/manageable-vault.helpers'; -import { mint } from '../common/mTBILL.helpers'; +} from '../../common/manageable-vault.helpers'; +import { mint } from '../../common/mtoken.helpers'; describe('LayerZero', function () { describe('MidasLzMintBurnOFTAdapter', () => { @@ -126,7 +127,12 @@ describe('LayerZero', function () { await sendOft( fixture, { amount: 1000000 }, - { revertMessage: 'WMAC: hasnt role' }, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION( + undefined, + fixture.mTBILL, + ), + }, ); }); @@ -149,6 +155,33 @@ describe('LayerZero', function () { await sendOft(fixture, { amount: 100 }, { revertOnDst: true }); }); + it('should fail: from A to B when sender is blacklisted', async () => { + const fixture = await loadFixture(layerZeroFixture); + const { owner, regularAccounts, accessControl, mTBILL } = fixture; + + const blacklisted = regularAccounts[0]; + + await mint( + { owner, tokenContract: mTBILL }, + blacklisted, + parseUnits('100', 18), + ); + + await blackList( + { blacklistable: mTBILL, accessControl, owner }, + blacklisted, + ); + + await sendOft( + fixture, + { amount: 100 }, + { + from: blacklisted, + revertCustomError: acErrors.WMAC_BLACKLISTED(undefined, mTBILL), + }, + ); + }); + it('should fail: send mTBILL from A to B with rate limit exceeded', async () => { const fixture = await loadFixture(layerZeroFixture); const { oftAdapterA, eidB } = fixture; @@ -168,9 +201,9 @@ describe('LayerZero', function () { fixture, { amount: 101 }, { - revertWithCustomError: { + revertCustomError: { contract: oftAdapterA, - error: 'RateLimitExceeded', + customErrorName: 'RateLimitExceeded', }, }, ); @@ -767,9 +800,9 @@ describe('LayerZero', function () { }, { overrideValue: '1', - revertWithCustomError: { + revertCustomError: { contract: composer, - error: 'NoMsgValueExpected', + customErrorName: 'NoMsgValueExpected', }, }, ); @@ -877,9 +910,9 @@ describe('LayerZero', function () { minAmountLD: parseUnits('19.6264444441', 18), }, { - revertWithCustomError: { + revertCustomError: { contract: oftAdapterA, - error: 'SlippageExceeded', + customErrorName: 'SlippageExceeded', }, }, ); @@ -1124,9 +1157,9 @@ describe('LayerZero', function () { }, { overrideValue: '1', - revertWithCustomError: { + revertCustomError: { contract: composer, - error: 'NoMsgValueExpected', + customErrorName: 'NoMsgValueExpected', }, }, ); @@ -1670,20 +1703,7 @@ describe('LayerZero', function () { ).eq(parseUnits('100', 8)); await expect(composer.redeemPublic(owner.address, parseUnits('50'), 0)) - .emit( - redemptionVault, - redemptionVault.interface.events[ - 'RedeemInstantWithCustomRecipient(address,address,address,uint256,uint256,uint256)' - ].name, - ) - .withArgs( - composer.address, - stableCoins.usdc.address, - owner.address, - parseUnits('50'), - 0, - parseUnits('100'), - ); + .not.reverted; }); }); @@ -1869,23 +1889,7 @@ describe('LayerZero', function () { 0, referrerId, ), - ) - .emit( - depositVault, - depositVault.interface.events[ - 'DepositInstantWithCustomRecipient(address,address,address,uint256,uint256,uint256,uint256,bytes32)' - ].name, - ) - .withArgs( - composer.address, - stableCoins.usdc.address, - owner.address, - parseUnits('100'), - parseUnits('100'), - 0, - parseUnits('50'), - referrerId, - ); + ).not.reverted; }); }); }); diff --git a/test/unit/mtoken.test.ts b/test/unit/mtoken.test.ts index 76807887..c6ce22e5 100644 --- a/test/unit/mtoken.test.ts +++ b/test/unit/mtoken.test.ts @@ -1,477 +1,2642 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { increase } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time'; +import { + days, + hours, +} from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time/duration'; import { expect } from 'chai'; +import { Contract } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; -import { MTokenNameEnum } from '../../config'; -import { getRolesForToken, getRolesNamesForToken } from '../../helpers/roles'; -import { acErrors, blackList } from '../common/ac.helpers'; -import { defaultDeploy, mTokenPermissionedFixture } from '../common/fixtures'; -import { burn, mint } from '../common/mTBILL.helpers'; -import { tokenContractsTests } from '../common/token.tests'; +import { mTokensMetadata } from '../../helpers/mtokens-metadata'; +import { encodeFnSelector } from '../../helpers/utils'; +import { MToken } from '../../typechain-types'; +import { + acErrors, + blackList, + NO_DELAY, + setPermissionRoleTester, + setRoleTimelocksTester, + setupGrantOperatorRole, + unBlackList, +} from '../common/ac.helpers'; +import { + adminPauseContractTest, + pauseGlobalTest, + pauseVault, + pauseVaultFn, +} from '../common/common.helpers'; +import { deployProxyContract } from '../common/deploy.helpers'; +import { + defaultDeploy, + DefaultFixture, + mTokenPermissionedFixture, +} from '../common/fixtures'; +import { + burn, + clawbackTest, + decreaseMintRateLimitTest, + increaseMintRateLimitTest, + removeMintRateLimitTest, + mint, + setClawbackReceiverTest, + setMetadataTest, +} from '../common/mtoken.helpers'; +import { + bulkScheduleTimelockOperationTester, + executeTimelockOperationTester, +} from '../common/timelock-manager.helpers'; -const mProducts = Object.values(MTokenNameEnum); +const DEFAULT_UNPAUSE_DELAY = 86400; -describe('Token contracts', () => { - mProducts.forEach((product) => { - describe(`${product}`, () => { - tokenContractsTests(product); +const MINT_SEL = encodeFnSelector('mint(address,uint256)'); +const BURN_SEL = encodeFnSelector('burn(address,uint256)'); +const MINT_GOVERNED_SEL = encodeFnSelector('mintGoverned(address,uint256)'); +const BURN_GOVERNED_SEL = encodeFnSelector('burnGoverned(address,uint256)'); +const TRANSFER_SEL = encodeFnSelector('transfer(address,uint256)'); +const TRANSFER_FROM_SEL = encodeFnSelector( + 'transferFrom(address,address,uint256)', +); +const CLAWBACK_SEL = encodeFnSelector('clawback(uint256,address)'); +const SET_METADATA_SEL = encodeFnSelector('setMetadata(bytes32,bytes)'); +const SET_CLAWBACK_RECEIVER_SEL = encodeFnSelector( + 'setClawbackReceiver(address)', +); +const INCREASE_MINT_RATE_LIMIT_SEL = encodeFnSelector( + 'increaseMintRateLimit(uint256,uint256)', +); +const DECREASE_MINT_RATE_LIMIT_SEL = encodeFnSelector( + 'decreaseMintRateLimit(uint256,uint256)', +); +const REMOVE_MINT_RATE_LIMIT_SEL = encodeFnSelector( + 'removeMintRateLimitConfig(uint256)', +); +const ERC20_PAUSABLE_PAUSED_STORAGE_SLOT = 101; +export const ERC20_PAUSED_MSG = 'ERC20Pausable: token transfer while paused'; +const PAUSE_TEST_AMOUNT = parseUnits('100'); + +const SET_NAME_SYMBOL_DELAY = 2 * 24 * 3600; +const SET_NAME_SYMBOL_SEL = encodeFnSelector('setNameSymbol(string,string)'); + +const toStorageSlotHex = (slot: number) => + '0x' + slot.toString(16).padStart(64, '0'); + +const PROXY_ADMIN_SLOT = + '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103'; + +const setProxyAdmin = (address: string, admin: string) => + ethers.provider.send('hardhat_setStorageAt', [ + address, + PROXY_ADMIN_SLOT, + ethers.utils.hexZeroPad(admin, 32), + ]); + +const setInitializedVersion = (address: string, version: number) => + ethers.provider.send('hardhat_setStorageAt', [ + address, + toStorageSlotHex(0), + ethers.utils.hexZeroPad(ethers.utils.hexlify(version), 32), + ]); + +const toBoolWordHex = (value: boolean) => + '0x' + (value ? '1' : '0').padStart(64, '0'); + +export const setErc20PausablePaused = async ( + tokenContract: Contract, + paused = true, +) => { + await ethers.provider.send('hardhat_setStorageAt', [ + tokenContract.address, + toStorageSlotHex(ERC20_PAUSABLE_PAUSED_STORAGE_SLOT), + toBoolWordHex(paused), + ]); + expect(await tokenContract.paused()).eq(paused); +}; + +const pausedRevert = (tokenContract: MToken, selector: string) => ({ + revertCustomError: { + customErrorName: 'Paused', + args: [tokenContract.address, selector], + }, +}); + +const adminUnpauseContractViaTimelock = async (fixture: DefaultFixture) => { + const { pauseManager, timelockManager, timelock, owner, accessControl } = + fixture; + const calldata = pauseManager.interface.encodeFunctionData( + 'contractAdminUnpause', + [fixture.tokenContract.address], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [pauseManager.address], + [calldata], + ); + await increase(DEFAULT_UNPAUSE_DELAY); + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + pauseManager.address, + calldata, + owner.address, + ); +}; + +describe(`mToken`, function () { + it('deployment', async () => { + const { mTBILL } = await loadFixture(defaultDeploy); + + const expected = mTokensMetadata['mTBILL']; + expect(await mTBILL.name()).eq(expected.name); + expect(await mTBILL.symbol()).eq(expected.symbol); + + expect(await mTBILL.paused()).eq(false); + + const limits = await mTBILL.getMintRateLimitStatuses(); + + expect(limits.length).eq(0); + }); + + it('roles', async () => { + const { mTBILL, roles } = await loadFixture(defaultDeploy); + + const tokenRoles = roles.tokenRoles.mTBILL; + + expect(await mTBILL.burnerRole()).eq(tokenRoles.burner); + expect(await mTBILL.minterRole()).eq(tokenRoles.minter); + + expect(await mTBILL.contractAdminRole()).eq(tokenRoles.tokenManager); + }); + + it('initialize and v2 initialize', async () => { + const { tokenContract, clawbackReceiver } = await loadFixture( + defaultDeploy, + ); + + await expect( + tokenContract.initialize( + ethers.constants.AddressZero, + clawbackReceiver.address, + mTokensMetadata.mTBILL.name, + mTokensMetadata.mTBILL.symbol, + ), + ).revertedWith('Initializable: contract is already initialized'); + + await expect( + tokenContract.initializeV2(clawbackReceiver.address), + ).to.revertedWith('Initializable: contract is already initialized'); + }); + + describe('initializeV2() proxy admin restriction', () => { + const deployMToken = async () => { + const { accessControl, clawbackReceiver } = await loadFixture( + defaultDeploy, + ); + + const mTBILL = await deployProxyContract( + 'mToken', + [ + accessControl.address, + clawbackReceiver.address, + mTokensMetadata.mTBILL.name, + mTokensMetadata.mTBILL.symbol, + ], + 'initialize', + [ + ethers.utils.id('M_TOKEN_MANAGER_ROLE'), + ethers.utils.id('M_TOKEN_MINTER_ROLE'), + ethers.utils.id('M_TOKEN_BURNER_ROLE'), + ], + ); + + return { mTBILL, clawbackReceiver }; + }; + + it('fresh deploy: initializeV2 runs while proxy admin is zero', async () => { + const { mTBILL, clawbackReceiver } = await deployMToken(); + + expect(await mTBILL.clawbackReceiver()).eq(clawbackReceiver.address); + }); + + it('should fail: when already reinitialized, even from the proxy admin', async () => { + const [, admin] = await ethers.getSigners(); + const { mTBILL, clawbackReceiver } = await deployMToken(); + + await setProxyAdmin(mTBILL.address, admin.address); + + await expect( + mTBILL.connect(admin).initializeV2(clawbackReceiver.address), + ).revertedWith('Initializable: contract is already initialized'); + }); + + it('should fail: when initialized and caller is not the proxy admin', async () => { + const [, admin, stranger] = await ethers.getSigners(); + const { mTBILL, clawbackReceiver } = await deployMToken(); + + await setProxyAdmin(mTBILL.address, admin.address); + await setInitializedVersion(mTBILL.address, 1); + + await expect( + mTBILL.connect(stranger).initializeV2(clawbackReceiver.address), + ).revertedWithCustomError(mTBILL, 'SenderNotProxyAdmin'); + }); + + it('when initialized and caller is the proxy admin', async () => { + const [, admin, newClawbackReceiver] = await ethers.getSigners(); + const { mTBILL } = await deployMToken(); + + await setProxyAdmin(mTBILL.address, admin.address); + await setInitializedVersion(mTBILL.address, 1); + + await mTBILL.connect(admin).initializeV2(newClawbackReceiver.address); + + expect(await mTBILL.clawbackReceiver()).eq(newClawbackReceiver.address); }); }); - describe('mTokenPermissioned (mTokenPermissionedTest)', () => { - describe('transfer()', () => { - it('should fail: transfer when sender is not greenlisted', async () => { - const baseFixture = await defaultDeploy(); - const { - owner, - accessControl, - regularAccounts, - mTokenPermissioned, - mTokenPermissionedRoles, - } = await loadFixture( - mTokenPermissionedFixture.bind(this, baseFixture), - ); - const from = regularAccounts[0]; - const to = regularAccounts[1]; + describe('setNameSymbol()', () => { + it('should fail: when called directly', async () => { + const { mTBILL, accessControl, owner } = await loadFixture(defaultDeploy); - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - from.address, - ); - await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); - await accessControl.revokeRole( - mTokenPermissionedRoles.greenlisted, - from.address, - ); - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - to.address, - ); + const tokenManagerRole = await mTBILL.contractAdminRole(); + const newName = 'Updated Token Name'; + const newSymbol = 'UPD'; - await expect( - mTokenPermissioned.connect(from).transfer(to.address, 1), - ).revertedWith(acErrors.WMAC_HASNT_ROLE); - }); + await expect(mTBILL.connect(owner).setNameSymbol(newName, newSymbol)) + .revertedWithCustomError(accessControl, 'SenderIsNotTimelock') + .withArgs(tokenManagerRole, SET_NAME_SYMBOL_SEL, owner.address); + }); - it('should fail: transfer when recipient is not greenlisted', async () => { - const baseFixture = await defaultDeploy(); - const { - owner, - accessControl, - regularAccounts, - mTokenPermissioned, - mTokenPermissionedRoles, - } = await loadFixture( - mTokenPermissionedFixture.bind(this, baseFixture), - ); + it('should always require 2 days timelock even if contract admin/function admin role delay is different (no timelock for example)', async () => { + const { mTBILL, accessControl, owner, timelock, timelockManager } = + await loadFixture(defaultDeploy); - const from = regularAccounts[0]; - const to = regularAccounts[1]; + const tokenManagerRole = await mTBILL.contractAdminRole(); + const functionRoleKey = await accessControl.permissionRoleKey( + tokenManagerRole, + mTBILL.address, + SET_NAME_SYMBOL_SEL, + ); + const newName = 'No Delay Override Name'; + const newSymbol = 'NDO'; - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - from.address, - ); - await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); + await setRoleTimelocksTester( + { accessControl, owner, timelock, timelockManager }, + [tokenManagerRole, functionRoleKey], + [NO_DELAY, NO_DELAY], + ); - await expect( - mTokenPermissioned.connect(from).transfer(to.address, 1), - ).revertedWith(acErrors.WMAC_HASNT_ROLE); - }); + await expect(mTBILL.connect(owner).setNameSymbol(newName, newSymbol)) + .revertedWithCustomError(accessControl, 'SenderIsNotTimelock') + .withArgs(tokenManagerRole, SET_NAME_SYMBOL_SEL, owner.address); - it('should fail: transfer when from is blacklisted', async () => { - const baseFixture = await defaultDeploy(); - const { - owner, - accessControl, - regularAccounts, - mTokenPermissioned, - mTokenPermissionedRoles, - } = await loadFixture( - mTokenPermissionedFixture.bind(this, baseFixture), - ); + const calldata = mTBILL.interface.encodeFunctionData('setNameSymbol', [ + newName, + newSymbol, + ]); - const from = regularAccounts[0]; - const to = regularAccounts[1]; + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [mTBILL.address], + [calldata], + ); - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - from.address, - ); - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - to.address, - ); - await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); - await blackList( - { - blacklistable: mTokenPermissioned, - accessControl, - owner, + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + mTBILL.address, + calldata, + owner.address, + { + revertCustomError: { + contract: timelockManager, + customErrorName: 'TimelockOperationNotReady', }, - from, - ); + }, + ); + + await increase(SET_NAME_SYMBOL_DELAY); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + mTBILL.address, + calldata, + owner.address, + ); + + expect(await mTBILL.name()).eq(newName); + expect(await mTBILL.symbol()).eq(newSymbol); + }); + + it('when called through timelock manager with 2 days delay', async () => { + const { mTBILL, accessControl, owner, timelock, timelockManager } = + await loadFixture(defaultDeploy); + + const newName = 'Timelock Updated Name'; + const newSymbol = 'TLUPD'; + const calldata = mTBILL.interface.encodeFunctionData('setNameSymbol', [ + newName, + newSymbol, + ]); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [mTBILL.address], + [calldata], + ); + + await increase(SET_NAME_SYMBOL_DELAY); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + mTBILL.address, + calldata, + owner.address, + ); + + expect(await mTBILL.name()).eq(newName); + expect(await mTBILL.symbol()).eq(newSymbol); + }); + }); + + describe('mint()', () => { + it('should fail: call from address without "mint operator" role', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + const caller = regularAccounts[0]; - await expect( - mTokenPermissioned.connect(from).transfer(to.address, 1), - ).revertedWith(acErrors.WMAC_HAS_ROLE); + await mint({ tokenContract, owner }, owner, 0, { + from: caller, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, }); + }); - it('should fail: transfer when token is paused', async () => { - const baseFixture = await defaultDeploy(); - const { - owner, - accessControl, - regularAccounts, - mTokenPermissioned, - mTokenPermissionedRoles, - } = await loadFixture( - mTokenPermissionedFixture.bind(this, baseFixture), - ); + it('call from address with "mint operator" role', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); - const from = regularAccounts[0]; - const to = regularAccounts[1]; + const amount = parseUnits('100'); + const to = regularAccounts[0].address; - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - from.address, - ); - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - to.address, - ); - await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); + await mint({ tokenContract, owner }, to, amount); + }); + + it('when 1h limit is set but not exceeded', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + const amount = parseUnits('100'); + const to = regularAccounts[0].address; + + await increaseMintRateLimitTest( + { tokenContract, owner }, + 3600, + parseUnits('10000'), + ); + await mint({ tokenContract, owner }, to, amount); + }); + + it('when 1h and 10h limit is set but not exceeded', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + const amount = parseUnits('100'); + const to = regularAccounts[0].address; + + await increaseMintRateLimitTest( + { tokenContract, owner }, + 3600, + parseUnits('1000'), + ); + await increaseMintRateLimitTest( + { tokenContract, owner }, + 3600 * 10, + parseUnits('10000'), + ); + + await mint({ tokenContract, owner }, to, amount); + }); - await mTokenPermissioned.connect(owner).pause(); + it('should fail: amount exceeds mint rate limit', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + const to = regularAccounts[0]; + const window = days(1); - await expect( - mTokenPermissioned.connect(from).transfer(to.address, 1), - ).revertedWith('ERC20Pausable: token transfer while paused'); + await increaseMintRateLimitTest({ tokenContract, owner }, window, 100); + await mint({ tokenContract, owner }, to, 100); + await mint({ tokenContract, owner }, to, 1, { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + args: [window, 0, 1], + }, }); + }); - it('should fail: mint when receiver is not greenlisted', async () => { - const baseFixture = await defaultDeploy(); - const { owner, regularAccounts, mTokenPermissioned } = - await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); + it('should fail: one of multiple mint rate limits is exceeded', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + const to = regularAccounts[0]; + const longWindow = days(1); + const shortWindow = 60; - await mint( - { tokenContract: mTokenPermissioned, owner }, - regularAccounts[0], - 1, - { revertMessage: acErrors.WMAC_HASNT_ROLE }, - ); + await increaseMintRateLimitTest( + { tokenContract, owner }, + longWindow, + 100, + ); + await increaseMintRateLimitTest( + { tokenContract, owner }, + shortWindow, + 50, + ); + + await mint({ tokenContract, owner }, to, 60, { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + args: [shortWindow, 50, 60], + }, }); + }); + + describe('mint() sliding rate limit (RateLimitLibrary)', () => { + const setupMintRateLimitFixture = async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); - it('transfer when both parties are greenlisted', async () => { - const baseFixture = await defaultDeploy(); - const { + return { owner, - accessControl, - regularAccounts, - mTokenPermissioned, - mTokenPermissionedRoles, - } = await loadFixture( - mTokenPermissionedFixture.bind(this, baseFixture), + tokenContract, + holder: regularAccounts[0], + recipient: regularAccounts[1], + }; + }; + + it('10h window: full consume, after 1h restores ~10% and second mint uses it', async () => { + const { owner, tokenContract, holder } = + await setupMintRateLimitFixture(); + + await increaseMintRateLimitTest( + { tokenContract, owner }, + hours(10), + parseUnits('1000'), ); - const from = regularAccounts[0]; - const to = regularAccounts[1]; + await mint({ tokenContract, owner }, holder, parseUnits('1000')); + + await increase(hours(1)); + + await mint({ tokenContract, owner }, holder, parseUnits('100')); + }); + + it('1d window: after 80% consumed and limit halved, mint fails', async () => { + const { owner, tokenContract, holder } = + await setupMintRateLimitFixture(); - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - from.address, + const window = days(1); + const initialLimit = parseUnits('1000'); + + await increaseMintRateLimitTest( + { tokenContract, owner }, + window, + initialLimit, ); - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - to.address, + + await mint({ tokenContract, owner }, holder, parseUnits('800')); + + await decreaseMintRateLimitTest( + { tokenContract, owner }, + window, + initialLimit.div(2), ); - await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); - await expect(mTokenPermissioned.connect(from).transfer(to.address, 1)) - .not.reverted; - expect(await mTokenPermissioned.balanceOf(to.address)).eq(1); + await mint({ tokenContract, owner }, holder, parseUnits('100'), { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + }, + }); }); - it('mint when receiver is greenlisted', async () => { - const baseFixture = await defaultDeploy(); - const { - owner, - accessControl, - regularAccounts, - mTokenPermissioned, - mTokenPermissionedRoles, - } = await loadFixture( - mTokenPermissionedFixture.bind(this, baseFixture), - ); + it('1d window: after limit halved, wait 12h and mint small amount', async () => { + const { owner, tokenContract, holder } = + await setupMintRateLimitFixture(); + + const window = days(1); + const initialLimit = parseUnits('1000'); - const to = regularAccounts[0]; - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - to.address, + await increaseMintRateLimitTest( + { tokenContract, owner }, + window, + initialLimit, ); - await mint( - { tokenContract: mTokenPermissioned, owner }, - to, - parseUnits('1'), + await mint({ tokenContract, owner }, holder, parseUnits('800')); + + await decreaseMintRateLimitTest( + { tokenContract, owner }, + window, + initialLimit.div(2), ); + + await mint({ tokenContract, owner }, holder, parseUnits('100'), { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + }, + }); + + await increase(hours(18)); + + await mint({ tokenContract, owner }, holder, parseUnits('1')); }); - it('burn without greenlist on holder', async () => { - const baseFixture = await defaultDeploy(); - const { - owner, - accessControl, - regularAccounts, - mTokenPermissioned, - mTokenPermissionedRoles, - } = await loadFixture( - mTokenPermissionedFixture.bind(this, baseFixture), - ); + it('multiple windows active at the same time', async () => { + const { owner, tokenContract, holder } = + await setupMintRateLimitFixture(); - const holder = regularAccounts[0]; - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - holder.address, + await increaseMintRateLimitTest( + { tokenContract, owner }, + hours(1), + parseUnits('100'), ); - await mint({ tokenContract: mTokenPermissioned, owner }, holder, 1); - await accessControl.revokeRole( - mTokenPermissionedRoles.greenlisted, - holder.address, + await increaseMintRateLimitTest( + { tokenContract, owner }, + hours(6), + parseUnits('500'), ); + await increaseMintRateLimitTest( + { tokenContract, owner }, + days(1), + parseUnits('10000'), + ); + + await mint({ tokenContract, owner }, holder, parseUnits('100')); - await burn({ tokenContract: mTokenPermissioned, owner }, holder, 1); + await mint({ tokenContract, owner }, holder, parseUnits('50'), { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + }, + }); + + await increase(hours(1)); + + await mint({ tokenContract, owner }, holder, parseUnits('50')); }); - }); - describe('transferFrom()', () => { - const greenlistComboCases: { - fromGreenlisted: boolean; - toGreenlisted: boolean; - callerGreenlisted: boolean; - expectSuccess: boolean; - }[] = [ - { - fromGreenlisted: true, - toGreenlisted: true, - callerGreenlisted: true, - expectSuccess: true, - }, - { - fromGreenlisted: true, - toGreenlisted: true, - callerGreenlisted: false, - expectSuccess: true, - }, - { - fromGreenlisted: false, - toGreenlisted: true, - callerGreenlisted: true, - expectSuccess: false, - }, - { - fromGreenlisted: false, - toGreenlisted: true, - callerGreenlisted: false, - expectSuccess: false, - }, - { - fromGreenlisted: false, - toGreenlisted: false, - callerGreenlisted: true, - expectSuccess: false, - }, - { - fromGreenlisted: false, - toGreenlisted: false, - callerGreenlisted: false, - expectSuccess: false, - }, - { - fromGreenlisted: true, - toGreenlisted: false, - callerGreenlisted: true, - expectSuccess: false, - }, - { - fromGreenlisted: true, - toGreenlisted: false, - callerGreenlisted: false, - expectSuccess: false, - }, - ]; - - greenlistComboCases.forEach( - ({ - fromGreenlisted, - toGreenlisted, - callerGreenlisted, - expectSuccess, - }) => { - const fromL = fromGreenlisted ? 'greenlisted' : 'not greenlisted'; - const toL = toGreenlisted ? 'greenlisted' : 'not greenlisted'; - const callerL = callerGreenlisted ? 'greenlisted' : 'not greenlisted'; - - it( - expectSuccess - ? `succeeds: from ${fromL}, to ${toL}, caller ${callerL}` - : `should fail: from ${fromL}, to ${toL}, caller ${callerL}`, - async () => { - const baseFixture = await defaultDeploy(); - const { - owner, - accessControl, - regularAccounts, - mTokenPermissioned, - mTokenPermissionedRoles, - } = await loadFixture( - mTokenPermissionedFixture.bind(this, baseFixture), - ); + it('burn is not affected when mint rate limit is exhausted', async () => { + const { owner, tokenContract, holder } = + await setupMintRateLimitFixture(); - const from = regularAccounts[0]; - const caller = regularAccounts[1]; - const to = regularAccounts[2]; - const { greenlisted } = mTokenPermissionedRoles; - - await accessControl.grantRole(greenlisted, from.address); - await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); - await mTokenPermissioned.connect(from).approve(caller.address, 1); - - if (!fromGreenlisted) { - await accessControl.revokeRole(greenlisted, from.address); - } - if (toGreenlisted) { - await accessControl.grantRole(greenlisted, to.address); - } - if (callerGreenlisted) { - await accessControl.grantRole(greenlisted, caller.address); - } - - const tx = mTokenPermissioned - .connect(caller) - .transferFrom(from.address, to.address, 1); - - if (expectSuccess) { - await expect(tx).not.reverted; - expect(await mTokenPermissioned.balanceOf(to.address)).eq(1); - } else { - await expect(tx).revertedWith(acErrors.WMAC_HASNT_ROLE); - } - }, - ); - }, - ); + const window = days(1); - it('should fail: transferFrom when from is blacklisted', async () => { - const baseFixture = await defaultDeploy(); - const { - owner, - accessControl, - regularAccounts, - mTokenPermissioned, - mTokenPermissionedRoles, - } = await loadFixture( - mTokenPermissionedFixture.bind(this, baseFixture), + await increaseMintRateLimitTest( + { tokenContract, owner }, + window, + parseUnits('100'), ); - const from = regularAccounts[0]; - const spender = regularAccounts[1]; - const to = regularAccounts[2]; + await mint({ tokenContract, owner }, holder, parseUnits('100')); - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - from.address, - ); - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - to.address, - ); - await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); - await blackList( - { - blacklistable: mTokenPermissioned, - accessControl, - owner, + await mint({ tokenContract, owner }, holder, parseUnits('1'), { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', }, - from, - ); - await mTokenPermissioned.connect(from).approve(spender.address, 1); + }); - await expect( - mTokenPermissioned - .connect(spender) - .transferFrom(from.address, to.address, 1), - ).revertedWith(acErrors.WMAC_HAS_ROLE); + await tokenContract + .connect(owner) + .burn(holder.address, parseUnits('50')); }); - it('should fail: transferFrom when to is blacklisted', async () => { - const baseFixture = await defaultDeploy(); - const { - owner, - accessControl, - regularAccounts, - mTokenPermissioned, - mTokenPermissionedRoles, - } = await loadFixture( - mTokenPermissionedFixture.bind(this, baseFixture), - ); + it('transfer is not affected when mint rate limit is exhausted', async () => { + const { owner, tokenContract, holder, recipient } = + await setupMintRateLimitFixture(); - const from = regularAccounts[0]; - const spender = regularAccounts[1]; - const to = regularAccounts[2]; + const window = days(1); - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - from.address, + await increaseMintRateLimitTest( + { tokenContract, owner }, + window, + parseUnits('100'), ); - await accessControl.grantRole( - mTokenPermissionedRoles.greenlisted, - to.address, - ); - await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); - await blackList( - { - blacklistable: mTokenPermissioned, - accessControl, - owner, + + await mint({ tokenContract, owner }, holder, parseUnits('100')); + + await mint({ tokenContract, owner }, holder, parseUnits('1'), { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', }, - to, - ); - await mTokenPermissioned.connect(from).approve(spender.address, 1); + }); - await expect( - mTokenPermissioned - .connect(spender) - .transferFrom(from.address, to.address, 1), - ).revertedWith(acErrors.WMAC_HAS_ROLE); + await tokenContract + .connect(holder) + .transfer(recipient.address, parseUnits('50')); }); }); - }); -}); -describe('Shared greenlist role (mGLO -> mGLOBAL)', () => { - it('mGLO greenlist role name is M_GLOBAL_GREENLISTED_ROLE', () => { - expect(getRolesNamesForToken('mGLO').greenlisted).eq( - 'M_GLOBAL_GREENLISTED_ROLE', - ); - }); + it('should fail: contract is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); - it('mGLO greenlist role hash equals mGLOBAL greenlist role hash', () => { - expect(getRolesForToken('mGLO').greenlisted).eq( - getRolesForToken('mGLOBAL').greenlisted, - ); + await pauseVault({ pauseManager, owner }, tokenContract); + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, MINT_SEL), + ); + }); + + it('should fail: mint is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await pauseVaultFn({ pauseManager, owner }, tokenContract, MINT_SEL); + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, MINT_SEL), + ); + }); + + it('should fail: globally paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await pauseGlobalTest({ pauseManager, owner }); + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, MINT_SEL), + ); + }); + + it('should fail: contract admin paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await adminPauseContractTest({ pauseManager, owner }, tokenContract); + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, MINT_SEL), + ); + }); + + it('should fail: mint when ERC20Pausable is paused', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + await setErc20PausablePaused(tokenContract); + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + { revertMessage: ERC20_PAUSED_MSG }, + ); + }); + + it('mint after contract admin unpause by pause manager', async () => { + const fixture = await loadFixture(defaultDeploy); + + await adminPauseContractTest( + { pauseManager: fixture.pauseManager, owner: fixture.owner }, + fixture.tokenContract, + ); + await adminUnpauseContractViaTimelock(fixture); + await mint( + { tokenContract: fixture.tokenContract, owner: fixture.owner }, + fixture.regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + }); }); - it('mGLO keeps its own (separated) operational roles', () => { - const mGloRoles = getRolesNamesForToken('mGLO'); - expect(mGloRoles.minter).eq('M_GLO_MINT_OPERATOR_ROLE'); - expect(mGloRoles.burner).eq('M_GLO_BURN_OPERATOR_ROLE'); - expect(mGloRoles.depositVaultAdmin).eq('M_GLO_DEPOSIT_VAULT_ADMIN_ROLE'); - expect(mGloRoles.redemptionVaultAdmin).eq( - 'M_GLO_REDEMPTION_VAULT_ADMIN_ROLE', - ); - expect(mGloRoles.customFeedAdmin).eq( - 'M_GLO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE', - ); + describe('burn()', () => { + it('should fail: call from address without "burn operator" role', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + const caller = regularAccounts[0]; + + await burn({ tokenContract, owner }, owner, 0, { + from: caller, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }); + }); + + it('should fail: call when user has insufficient balance', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + const amount = parseUnits('100'); + const to = regularAccounts[0].address; + + await burn({ tokenContract, owner }, to, amount, { + revertMessage: 'ERC20: burn amount exceeds balance', + }); + }); + + it('call from address with "mint operator" role', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + const amount = parseUnits('100'); + const to = regularAccounts[0].address; + + await mint({ tokenContract, owner }, to, amount); + await burn({ tokenContract, owner }, to, amount); + }); + + it('burn is not affected by mint rate limits', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + const holder = regularAccounts[0]; + const window = days(1); + + await increaseMintRateLimitTest({ tokenContract, owner }, window, 100); + await mint({ tokenContract, owner }, holder, 100); + await mint({ tokenContract, owner }, holder, 1, { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + args: [window, 0, 1], + }, + }); + + await burn({ tokenContract, owner }, holder, 50); + }); + + it('should fail: burn when contract is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await pauseVault({ pauseManager, owner }, tokenContract); + await burn( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, BURN_SEL), + ); + }); + + it('should fail: burn when burn is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await pauseVaultFn({ pauseManager, owner }, tokenContract, BURN_SEL); + await burn( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, BURN_SEL), + ); + }); + + it('should fail: burn when globally paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await pauseGlobalTest({ pauseManager, owner }); + await burn( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, BURN_SEL), + ); + }); + + it('should fail: burn when contract admin paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await adminPauseContractTest({ pauseManager, owner }, tokenContract); + await burn( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, BURN_SEL), + ); + }); + + it('should fail: burn when ERC20Pausable is paused', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await setErc20PausablePaused(tokenContract); + await burn( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + { revertMessage: ERC20_PAUSED_MSG }, + ); + }); + }); + + describe('mintGoverned()', () => { + it('should fail: mintGoverned when contract is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await pauseVault({ pauseManager, owner }, tokenContract); + await mint( + { tokenContract, owner, isGoverned: true }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, MINT_GOVERNED_SEL), + ); + }); + + it('should fail: mintGoverned when mintGoverned is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await pauseVaultFn( + { pauseManager, owner }, + tokenContract, + MINT_GOVERNED_SEL, + ); + + await mint( + { tokenContract, owner, isGoverned: true }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, MINT_GOVERNED_SEL), + ); + }); + + it('should fail: mintGoverned when globally paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await pauseGlobalTest({ pauseManager, owner }); + await mint( + { tokenContract, owner, isGoverned: true }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, MINT_GOVERNED_SEL), + ); + }); + + it('should fail: mintGoverned when contract admin paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await adminPauseContractTest({ pauseManager, owner }, tokenContract); + await mint( + { tokenContract, owner, isGoverned: true }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, MINT_GOVERNED_SEL), + ); + }); + + it('should fail: mintGoverned when ERC20Pausable is paused', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + await setErc20PausablePaused(tokenContract); + await mint( + { tokenContract, owner, isGoverned: true }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + { revertMessage: ERC20_PAUSED_MSG }, + ); + }); + }); + + describe('burnGoverned()', () => { + it('should fail: burnGoverned when contract is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await pauseVault({ pauseManager, owner }, tokenContract); + await burn( + { tokenContract, owner, isGoverned: true }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, BURN_GOVERNED_SEL), + ); + }); + + it('should fail: burnGoverned when burnGoverned is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await pauseVaultFn( + { pauseManager, owner }, + tokenContract, + BURN_GOVERNED_SEL, + ); + await burn( + { tokenContract, owner, isGoverned: true }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, BURN_GOVERNED_SEL), + ); + }); + + it('should fail: burnGoverned when globally paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await pauseGlobalTest({ pauseManager, owner }); + await burn( + { tokenContract, owner, isGoverned: true }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, BURN_GOVERNED_SEL), + ); + }); + + it('should fail: burnGoverned when contract admin paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await adminPauseContractTest({ pauseManager, owner }, tokenContract); + await burn( + { tokenContract, owner, isGoverned: true }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + pausedRevert(tokenContract, BURN_GOVERNED_SEL), + ); + }); + + it('should fail: burnGoverned when ERC20Pausable is paused', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await setErc20PausablePaused(tokenContract); + await burn( + { tokenContract, owner, isGoverned: true }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + { revertMessage: ERC20_PAUSED_MSG }, + ); + }); + }); + + describe('transfer()', () => { + it('should fail: transfer when contract is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint({ tokenContract, owner }, owner, PAUSE_TEST_AMOUNT); + await pauseVault({ pauseManager, owner }, tokenContract); + await expect( + tokenContract + .connect(owner) + .transfer(regularAccounts[0].address, PAUSE_TEST_AMOUNT), + ) + .revertedWithCustomError(tokenContract, 'Paused') + .withArgs(tokenContract.address, TRANSFER_SEL); + }); + + it('should fail: transfer when transfer is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint({ tokenContract, owner }, owner, PAUSE_TEST_AMOUNT); + await pauseVaultFn({ pauseManager, owner }, tokenContract, TRANSFER_SEL); + await expect( + tokenContract + .connect(owner) + .transfer(regularAccounts[0].address, PAUSE_TEST_AMOUNT), + ) + .revertedWithCustomError(tokenContract, 'Paused') + .withArgs(tokenContract.address, TRANSFER_SEL); + }); + + it('should fail: transfer when globally paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint({ tokenContract, owner }, owner, PAUSE_TEST_AMOUNT); + await pauseGlobalTest({ pauseManager, owner }); + await expect( + tokenContract + .connect(owner) + .transfer(regularAccounts[0].address, PAUSE_TEST_AMOUNT), + ) + .revertedWithCustomError(tokenContract, 'Paused') + .withArgs(tokenContract.address, TRANSFER_SEL); + }); + + it('should fail: transfer when contract admin paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint({ tokenContract, owner }, owner, PAUSE_TEST_AMOUNT); + await adminPauseContractTest({ pauseManager, owner }, tokenContract); + await expect( + tokenContract + .connect(owner) + .transfer(regularAccounts[0].address, PAUSE_TEST_AMOUNT), + ) + .revertedWithCustomError(tokenContract, 'Paused') + .withArgs(tokenContract.address, TRANSFER_SEL); + }); + + it('should fail: transfer when ERC20Pausable is paused', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + await mint({ tokenContract, owner }, owner, PAUSE_TEST_AMOUNT); + await setErc20PausablePaused(tokenContract); + await expect( + tokenContract + .connect(owner) + .transfer(regularAccounts[0].address, PAUSE_TEST_AMOUNT), + ).revertedWith(ERC20_PAUSED_MSG); + }); + }); + + describe('transferFrom()', () => { + it('should fail: transferFrom when contract is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await tokenContract + .connect(regularAccounts[0]) + .approve(regularAccounts[1].address, PAUSE_TEST_AMOUNT); + await pauseVault({ pauseManager, owner }, tokenContract); + await expect( + tokenContract + .connect(regularAccounts[1]) + .transferFrom( + regularAccounts[0].address, + owner.address, + PAUSE_TEST_AMOUNT, + ), + ) + .revertedWithCustomError(tokenContract, 'Paused') + .withArgs(tokenContract.address, TRANSFER_FROM_SEL); + }); + + it('should fail: transferFrom when transferFrom is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await tokenContract + .connect(regularAccounts[0]) + .approve(regularAccounts[1].address, PAUSE_TEST_AMOUNT); + await pauseVaultFn( + { pauseManager, owner }, + tokenContract, + TRANSFER_FROM_SEL, + ); + await expect( + tokenContract + .connect(regularAccounts[1]) + .transferFrom( + regularAccounts[0].address, + owner.address, + PAUSE_TEST_AMOUNT, + ), + ) + .revertedWithCustomError(tokenContract, 'Paused') + .withArgs(tokenContract.address, TRANSFER_FROM_SEL); + }); + + it('should fail: transferFrom when globally paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await tokenContract + .connect(regularAccounts[0]) + .approve(regularAccounts[1].address, PAUSE_TEST_AMOUNT); + await pauseGlobalTest({ pauseManager, owner }); + await expect( + tokenContract + .connect(regularAccounts[1]) + .transferFrom( + regularAccounts[0].address, + owner.address, + PAUSE_TEST_AMOUNT, + ), + ) + .revertedWithCustomError(tokenContract, 'Paused') + .withArgs(tokenContract.address, TRANSFER_FROM_SEL); + }); + + it('should fail: transferFrom when contract admin paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await tokenContract + .connect(regularAccounts[0]) + .approve(regularAccounts[1].address, PAUSE_TEST_AMOUNT); + await adminPauseContractTest({ pauseManager, owner }, tokenContract); + await expect( + tokenContract + .connect(regularAccounts[1]) + .transferFrom( + regularAccounts[0].address, + owner.address, + PAUSE_TEST_AMOUNT, + ), + ) + .revertedWithCustomError(tokenContract, 'Paused') + .withArgs(tokenContract.address, TRANSFER_FROM_SEL); + }); + + it('should fail: transferFrom when ERC20Pausable is paused', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + await mint( + { tokenContract, owner }, + regularAccounts[0], + PAUSE_TEST_AMOUNT, + ); + await tokenContract + .connect(regularAccounts[0]) + .approve(regularAccounts[1].address, PAUSE_TEST_AMOUNT); + await setErc20PausablePaused(tokenContract); + await expect( + tokenContract + .connect(regularAccounts[1]) + .transferFrom( + regularAccounts[0].address, + owner.address, + PAUSE_TEST_AMOUNT, + ), + ).revertedWith(ERC20_PAUSED_MSG); + }); + }); + + describe('setMetadata()', () => { + it('should fail: call from address without token manager role', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + const caller = regularAccounts[0]; + + await setMetadataTest({ tokenContract, owner }, 'url', 'some value', { + from: caller, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }); + }); + + it('call from address with token manager role', async () => { + const { owner, tokenContract } = await loadFixture(defaultDeploy); + + await setMetadataTest( + { tokenContract, owner }, + 'url', + 'some value', + undefined, + ); + }); + + it('call when globally paused by pause manager', async () => { + const { pauseManager, owner, tokenContract } = await loadFixture( + defaultDeploy, + ); + + await pauseGlobalTest({ pauseManager, owner }); + await setMetadataTest( + { tokenContract, owner }, + 'url', + 'some value', + undefined, + ); + }); + + it('call when contract is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract } = await loadFixture( + defaultDeploy, + ); + + await pauseVault({ pauseManager, owner }, tokenContract); + await setMetadataTest( + { tokenContract, owner }, + 'url', + 'some value', + undefined, + ); + }); + + it('call when setMetadata is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract } = await loadFixture( + defaultDeploy, + ); + + await pauseVaultFn( + { pauseManager, owner }, + tokenContract, + SET_METADATA_SEL, + ); + await setMetadataTest( + { tokenContract, owner }, + 'url', + 'some value', + undefined, + ); + }); + }); + + describe('setClawbackReceiver()', () => { + it('should fail: call from address without token manager role nor function permission', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + const caller = regularAccounts[0]; + + await setClawbackReceiverTest( + { tokenContract, owner }, + regularAccounts[1].address, + { + from: caller, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: new clawback receiver cannot be address zero', async () => { + const { owner, tokenContract } = await loadFixture(defaultDeploy); + + await setClawbackReceiverTest( + { tokenContract, owner }, + ethers.constants.AddressZero, + { revertCustomError: { customErrorName: 'InvalidAddress' } }, + ); + }); + + it('call from address with token manager role', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + await setClawbackReceiverTest( + { tokenContract, owner }, + regularAccounts[2].address, + undefined, + ); + }); + + it('call from address with scoped function permission only', async () => { + const { owner, tokenContract, regularAccounts, accessControl, roles } = + await loadFixture(defaultDeploy); + + const user = regularAccounts[0]; + const nextReceiver = regularAccounts[3].address; + const selector = encodeFnSelector('setClawbackReceiver(address)'); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.tokenRoles.mTBILL.tokenManager, + targetContract: tokenContract.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + tokenContract.address, + selector, + [{ account: user.address, enabled: true }], + ); + + expect( + await accessControl.hasRole(roles.common.defaultAdmin, user.address), + ).eq(false); + + await setClawbackReceiverTest({ tokenContract, owner }, nextReceiver, { + from: user, + }); + }); + + it('call when globally paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await pauseGlobalTest({ pauseManager, owner }); + await setClawbackReceiverTest( + { tokenContract, owner }, + regularAccounts[1].address, + undefined, + ); + }); + + it('call when contract is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await pauseVault({ pauseManager, owner }, tokenContract); + await setClawbackReceiverTest( + { tokenContract, owner }, + regularAccounts[1].address, + undefined, + ); + }); + + it('call when setClawbackReceiver is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + await pauseVaultFn( + { pauseManager, owner }, + tokenContract, + SET_CLAWBACK_RECEIVER_SEL, + ); + await setClawbackReceiverTest( + { tokenContract, owner }, + regularAccounts[1].address, + undefined, + ); + }); + }); + + describe('clawback()', () => { + it('should fail: call from address without token manager role nor function permission', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + const caller = regularAccounts[0]; + const victim = regularAccounts[1]; + const amount = parseUnits('1'); + + await mint({ tokenContract, owner }, victim, amount); + + await clawbackTest({ tokenContract, owner }, amount, victim, { + from: caller, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }); + }); + + it('should not fail when from address is blacklisted', async () => { + const { owner, tokenContract, regularAccounts, accessControl } = + await loadFixture(defaultDeploy); + + const holder = regularAccounts[0]; + const amount = parseUnits('10'); + + await mint({ tokenContract, owner }, holder, amount); + await blackList( + { blacklistable: tokenContract, accessControl, owner }, + holder, + ); + + await clawbackTest({ tokenContract, owner }, amount, holder, undefined); + + expect(await tokenContract.balanceOf(holder.address)).eq(0); + }); + + it('should fail: when clawbackReceiver is blacklisted', async () => { + const { owner, tokenContract, regularAccounts, accessControl } = + await loadFixture(defaultDeploy); + + const holder = regularAccounts[0]; + const amount = parseUnits('5'); + + await mint({ tokenContract, owner }, holder, amount); + + await blackList( + { blacklistable: tokenContract, accessControl, owner }, + regularAccounts[2], + ); + await setClawbackReceiverTest( + { tokenContract, owner }, + regularAccounts[2].address, + undefined, + ); + + await clawbackTest({ tokenContract, owner }, amount, holder, { + revertCustomError: acErrors.WMAC_BLACKLISTED, + }); + }); + + it('call from address with token manager role', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + const holder = regularAccounts[0]; + const amount = parseUnits('7'); + + await mint({ tokenContract, owner }, holder, amount); + await clawbackTest({ tokenContract, owner }, amount, holder, undefined); + }); + + it('call from address with scoped function permission only', async () => { + const { owner, tokenContract, regularAccounts, accessControl, roles } = + await loadFixture(defaultDeploy); + + const operator = regularAccounts[0]; + const holder = regularAccounts[1]; + const amount = parseUnits('3'); + const selector = encodeFnSelector('clawback(uint256,address)'); + + await mint({ tokenContract, owner }, holder, amount); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: roles.tokenRoles.mTBILL.tokenManager, + targetContract: tokenContract.address, + functionSelector: selector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + undefined, + tokenContract.address, + selector, + [{ account: operator.address, enabled: true }], + ); + + expect( + await accessControl.hasRole( + roles.tokenRoles.mTBILL.tokenManager, + operator.address, + ), + ).eq(false); + + await clawbackTest({ tokenContract, owner }, amount, holder, { + from: operator, + }); + }); + + it('should fail: clawback when contract is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + const holder = regularAccounts[0]; + await mint({ tokenContract, owner }, holder, PAUSE_TEST_AMOUNT); + await pauseVault({ pauseManager, owner }, tokenContract); + await clawbackTest( + { tokenContract, owner }, + PAUSE_TEST_AMOUNT, + holder, + pausedRevert(tokenContract, CLAWBACK_SEL), + ); + }); + + it('should fail: clawback when clawback is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + const holder = regularAccounts[0]; + await mint({ tokenContract, owner }, holder, PAUSE_TEST_AMOUNT); + await pauseVaultFn({ pauseManager, owner }, tokenContract, CLAWBACK_SEL); + await clawbackTest( + { tokenContract, owner }, + PAUSE_TEST_AMOUNT, + holder, + pausedRevert(tokenContract, CLAWBACK_SEL), + ); + }); + + it('should fail: clawback when globally paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + const holder = regularAccounts[0]; + await mint({ tokenContract, owner }, holder, PAUSE_TEST_AMOUNT); + await pauseGlobalTest({ pauseManager, owner }); + await clawbackTest( + { tokenContract, owner }, + PAUSE_TEST_AMOUNT, + holder, + pausedRevert(tokenContract, CLAWBACK_SEL), + ); + }); + + it('should fail: clawback when contract admin paused by pause manager', async () => { + const { pauseManager, owner, tokenContract, regularAccounts } = + await loadFixture(defaultDeploy); + + const holder = regularAccounts[0]; + await mint({ tokenContract, owner }, holder, PAUSE_TEST_AMOUNT); + await adminPauseContractTest({ pauseManager, owner }, tokenContract); + await clawbackTest( + { tokenContract, owner }, + PAUSE_TEST_AMOUNT, + holder, + pausedRevert(tokenContract, CLAWBACK_SEL), + ); + }); + + it('should fail: clawback when ERC20Pausable is paused', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + const holder = regularAccounts[0]; + await mint({ tokenContract, owner }, holder, PAUSE_TEST_AMOUNT); + await setErc20PausablePaused(tokenContract); + await clawbackTest({ tokenContract, owner }, PAUSE_TEST_AMOUNT, holder, { + revertMessage: ERC20_PAUSED_MSG, + }); + }); + }); + + describe('increaseMintRateLimit()', () => { + it('should fail: call from address without token manager role', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + const caller = regularAccounts[0]; + + await increaseMintRateLimitTest({ tokenContract, owner }, days(1), 1, { + from: caller, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }); + }); + + it('should fail: call with new limit <= existing limit', async () => { + const { owner, tokenContract } = await loadFixture(defaultDeploy); + const window = days(1); + + await increaseMintRateLimitTest({ tokenContract, owner }, window, 100); + await increaseMintRateLimitTest({ tokenContract, owner }, window, 100, { + revertCustomError: { + customErrorName: 'InvalidNewLimit', + args: [100, 100], + }, + }); + }); + + it('call from address with token manager role', async () => { + const { owner, tokenContract } = await loadFixture(defaultDeploy); + const window = days(1); + + await increaseMintRateLimitTest({ tokenContract, owner }, window, 100); + await increaseMintRateLimitTest({ tokenContract, owner }, window, 200); + }); + + it('should fail: when window is shorter than 1 minute', async () => { + const { owner, tokenContract } = await loadFixture(defaultDeploy); + + await increaseMintRateLimitTest( + { tokenContract, owner }, + 59, + parseUnits('1000'), + { + revertCustomError: { + customErrorName: 'WindowTooShort', + args: [59], + }, + }, + ); + }); + + it('call when globally paused by pause manager', async () => { + const { pauseManager, owner, tokenContract } = await loadFixture( + defaultDeploy, + ); + + await pauseGlobalTest({ pauseManager, owner }); + await increaseMintRateLimitTest( + { tokenContract, owner }, + days(1), + parseUnits('1000'), + ); + }); + + it('call when contract is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract } = await loadFixture( + defaultDeploy, + ); + + await pauseVault({ pauseManager, owner }, tokenContract); + await increaseMintRateLimitTest( + { tokenContract, owner }, + days(1), + parseUnits('1000'), + ); + }); + + it('call when increaseMintRateLimit is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract } = await loadFixture( + defaultDeploy, + ); + + await pauseVaultFn( + { pauseManager, owner }, + tokenContract, + INCREASE_MINT_RATE_LIMIT_SEL, + ); + await increaseMintRateLimitTest( + { tokenContract, owner }, + days(1), + parseUnits('1000'), + ); + }); + }); + + describe('decreaseMintRateLimit()', () => { + it('should fail: call from address without token manager role', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + const caller = regularAccounts[0]; + + await decreaseMintRateLimitTest({ tokenContract, owner }, days(1), 1, { + from: caller, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }); + }); + + it('should fail: call with new limit >= existing limit', async () => { + const { owner, tokenContract } = await loadFixture(defaultDeploy); + const window = days(1); + + await increaseMintRateLimitTest({ tokenContract, owner }, window, 100); + await decreaseMintRateLimitTest({ tokenContract, owner }, window, 100, { + revertCustomError: { + customErrorName: 'InvalidNewLimit', + args: [100, 100], + }, + }); + }); + + it('call from address with token manager role', async () => { + const { owner, tokenContract } = await loadFixture(defaultDeploy); + const window = days(1); + + await increaseMintRateLimitTest({ tokenContract, owner }, window, 200); + await decreaseMintRateLimitTest({ tokenContract, owner }, window, 100); + }); + + it('call when globally paused by pause manager', async () => { + const { pauseManager, owner, tokenContract } = await loadFixture( + defaultDeploy, + ); + const window = days(1); + + await increaseMintRateLimitTest({ tokenContract, owner }, window, 200); + await pauseGlobalTest({ pauseManager, owner }); + await decreaseMintRateLimitTest({ tokenContract, owner }, window, 100); + }); + + it('call when contract is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract } = await loadFixture( + defaultDeploy, + ); + const window = days(1); + + await increaseMintRateLimitTest({ tokenContract, owner }, window, 200); + await pauseVault({ pauseManager, owner }, tokenContract); + await decreaseMintRateLimitTest({ tokenContract, owner }, window, 100); + }); + + it('call when decreaseMintRateLimit is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract } = await loadFixture( + defaultDeploy, + ); + const window = days(1); + + await increaseMintRateLimitTest({ tokenContract, owner }, window, 200); + await pauseVaultFn( + { pauseManager, owner }, + tokenContract, + DECREASE_MINT_RATE_LIMIT_SEL, + ); + await decreaseMintRateLimitTest({ tokenContract, owner }, window, 100); + }); + }); + + describe('removeMintRateLimitConfig()', () => { + it('should fail: call from address without token manager role', async () => { + const { owner, tokenContract, regularAccounts } = await loadFixture( + defaultDeploy, + ); + + const caller = regularAccounts[0]; + const window = days(1); + + await increaseMintRateLimitTest({ tokenContract, owner }, window, 100); + await removeMintRateLimitTest({ tokenContract, owner }, window, { + from: caller, + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }); + }); + + it('should fail: when window does not exist', async () => { + const { owner, tokenContract } = await loadFixture(defaultDeploy); + + await removeMintRateLimitTest({ tokenContract, owner }, days(99), { + revertCustomError: { + customErrorName: 'UnknownWindowLimit', + }, + }); + }); + + it('call from address with token manager role', async () => { + const { owner, tokenContract } = await loadFixture(defaultDeploy); + const window = days(1); + + await increaseMintRateLimitTest({ tokenContract, owner }, window, 100); + await removeMintRateLimitTest({ tokenContract, owner }, window); + }); + + it('call when globally paused by pause manager', async () => { + const { pauseManager, owner, tokenContract } = await loadFixture( + defaultDeploy, + ); + const window = days(1); + + await increaseMintRateLimitTest({ tokenContract, owner }, window, 100); + await pauseGlobalTest({ pauseManager, owner }); + await removeMintRateLimitTest({ tokenContract, owner }, window); + }); + + it('call when contract is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract } = await loadFixture( + defaultDeploy, + ); + const window = days(1); + + await increaseMintRateLimitTest({ tokenContract, owner }, window, 100); + await pauseVault({ pauseManager, owner }, tokenContract); + await removeMintRateLimitTest({ tokenContract, owner }, window); + }); + + it('call when removeMintRateLimitConfig is paused by pause manager', async () => { + const { pauseManager, owner, tokenContract } = await loadFixture( + defaultDeploy, + ); + const window = days(1); + + await increaseMintRateLimitTest({ tokenContract, owner }, window, 100); + await pauseVaultFn( + { pauseManager, owner }, + tokenContract, + REMOVE_MINT_RATE_LIMIT_SEL, + ); + await removeMintRateLimitTest({ tokenContract, owner }, window); + }); + }); + + describe('_beforeTokenTransfer()', () => { + it('should fail: mint(...) when address is blacklisted', async () => { + const { owner, regularAccounts, accessControl, tokenContract } = + await loadFixture(defaultDeploy); + const blacklisted = regularAccounts[0]; + + await blackList( + { blacklistable: tokenContract, accessControl, owner }, + blacklisted, + ); + await mint({ tokenContract, owner }, blacklisted, 1, { + revertCustomError: acErrors.WMAC_BLACKLISTED, + }); + }); + + it('should fail: transfer(...) when from address is blacklisted', async () => { + const { owner, regularAccounts, accessControl, tokenContract } = + await loadFixture(defaultDeploy); + + const blacklisted = regularAccounts[0]; + const to = regularAccounts[1]; + + await mint({ tokenContract, owner }, blacklisted, 1); + await blackList( + { blacklistable: tokenContract, accessControl, owner }, + blacklisted, + ); + + await expect( + tokenContract.connect(blacklisted).transfer(to.address, 1), + ).revertedWithCustomError( + tokenContract, + acErrors.WMAC_BLACKLISTED().customErrorName, + ); + }); + + it('should fail: transfer(...) when to address is blacklisted', async () => { + const { owner, regularAccounts, accessControl, tokenContract } = + await loadFixture(defaultDeploy); + + const blacklisted = regularAccounts[0]; + const from = regularAccounts[1]; + + await mint({ tokenContract, owner }, from, 1); + await blackList( + { blacklistable: tokenContract, accessControl, owner }, + blacklisted, + ); + + await expect( + tokenContract.connect(from).transfer(blacklisted.address, 1), + ).revertedWithCustomError( + tokenContract, + acErrors.WMAC_BLACKLISTED().customErrorName, + ); + }); + + it('should fail: transferFrom(...) when from address is blacklisted', async () => { + const { owner, regularAccounts, accessControl, tokenContract } = + await loadFixture(defaultDeploy); + + const blacklisted = regularAccounts[0]; + const to = regularAccounts[1]; + + await mint({ tokenContract, owner }, blacklisted, 1); + await blackList( + { blacklistable: tokenContract, accessControl, owner }, + blacklisted, + ); + + await tokenContract.connect(blacklisted).approve(to.address, 1); + + await expect( + tokenContract + .connect(to) + .transferFrom(blacklisted.address, to.address, 1), + ).revertedWithCustomError( + tokenContract, + acErrors.WMAC_BLACKLISTED().customErrorName, + ); + }); + + it('should fail: transferFrom(...) when to address is blacklisted', async () => { + const { owner, regularAccounts, accessControl, tokenContract } = + await loadFixture(defaultDeploy); + + const blacklisted = regularAccounts[0]; + const from = regularAccounts[1]; + const caller = regularAccounts[2]; + + await mint({ tokenContract, owner }, from, 1); + + await blackList( + { blacklistable: tokenContract, accessControl, owner }, + blacklisted, + ); + await tokenContract.connect(from).approve(caller.address, 1); + + await expect( + tokenContract + .connect(caller) + .transferFrom(from.address, blacklisted.address, 1), + ).revertedWithCustomError( + tokenContract, + acErrors.WMAC_BLACKLISTED().customErrorName, + ); + }); + + it('should fail: burn(...) when address is blacklisted', async () => { + const { owner, regularAccounts, accessControl, tokenContract } = + await loadFixture(defaultDeploy); + + const blacklisted = regularAccounts[0]; + + await mint({ tokenContract, owner }, blacklisted, 1); + await blackList( + { blacklistable: tokenContract, accessControl, owner }, + blacklisted, + ); + await burn({ tokenContract, owner }, blacklisted, 1, { + revertCustomError: acErrors.WMAC_BLACKLISTED, + }); + }); + + it('transferFrom(...) when caller address is blacklisted', async () => { + const { owner, regularAccounts, accessControl, tokenContract } = + await loadFixture(defaultDeploy); + + const blacklisted = regularAccounts[0]; + const from = regularAccounts[1]; + const to = regularAccounts[2]; + + await mint({ tokenContract, owner }, from, 1); + await blackList( + { blacklistable: tokenContract, accessControl, owner }, + blacklisted, + ); + + await tokenContract.connect(from).approve(blacklisted.address, 1); + + await expect( + tokenContract + .connect(blacklisted) + .transferFrom(from.address, to.address, 1), + ).not.reverted; + }); + + it('transfer(...) when caller address was blacklisted and then un-blacklisted', async () => { + const { owner, regularAccounts, accessControl, tokenContract } = + await loadFixture(defaultDeploy); + + const blacklisted = regularAccounts[0]; + const to = regularAccounts[2]; + + await mint({ tokenContract, owner }, blacklisted, 1); + await blackList( + { blacklistable: tokenContract, accessControl, owner }, + blacklisted, + ); + + await expect( + tokenContract.connect(blacklisted).transfer(to.address, 1), + ).revertedWithCustomError( + tokenContract, + acErrors.WMAC_BLACKLISTED().customErrorName, + ); + + await unBlackList( + { blacklistable: tokenContract, accessControl, owner }, + blacklisted, + ); + + await expect(tokenContract.connect(blacklisted).transfer(to.address, 1)) + .not.reverted; + }); + + it('transfer(...) is not affected by mint rate limits set to 0', async () => { + const { owner, regularAccounts, tokenContract } = await loadFixture( + defaultDeploy, + ); + const from = regularAccounts[0]; + const to = regularAccounts[1]; + const dayWindow = days(1); + const minuteWindow = 60; + + await mint({ tokenContract, owner }, from, 1); + + await increaseMintRateLimitTest({ tokenContract, owner }, dayWindow, 1); + await decreaseMintRateLimitTest({ tokenContract, owner }, dayWindow, 0); + await increaseMintRateLimitTest( + { tokenContract, owner }, + minuteWindow, + 1, + ); + await decreaseMintRateLimitTest( + { tokenContract, owner }, + minuteWindow, + 0, + ); + + await increaseMintRateLimitTest({ tokenContract, owner }, dayWindow, 1); + await decreaseMintRateLimitTest({ tokenContract, owner }, dayWindow, 0); + await increaseMintRateLimitTest( + { tokenContract, owner }, + minuteWindow, + 1, + ); + await decreaseMintRateLimitTest( + { tokenContract, owner }, + minuteWindow, + 0, + ); + + await expect(tokenContract.connect(from).transfer(to.address, 1)).not + .reverted; + expect(await tokenContract.balanceOf(to.address)).eq(1); + }); + }); +}); + +describe('mTokenPermissioned', () => { + describe('transfer()', () => { + it('should fail: transfer when sender is not greenlisted', async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissioned, + mTokenPermissionedRoles, + } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + from.address, + ); + await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); + await accessControl.revokeRole( + mTokenPermissionedRoles.greenlisted, + from.address, + ); + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + to.address, + ); + + await expect( + mTokenPermissioned.connect(from).transfer(to.address, 1), + ).revertedWithCustomError(mTokenPermissioned, 'NotGreenlisted'); + }); + + it('should fail: transfer when recipient is not greenlisted', async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissioned, + mTokenPermissionedRoles, + } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + from.address, + ); + await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); + + await expect( + mTokenPermissioned.connect(from).transfer(to.address, 1), + ).revertedWithCustomError(mTokenPermissioned, 'NotGreenlisted'); + }); + + it('should fail: transfer when from is blacklisted', async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissioned, + mTokenPermissionedRoles, + } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + from.address, + ); + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + to.address, + ); + await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); + await blackList( + { + blacklistable: mTokenPermissioned, + accessControl, + owner, + }, + from, + ); + + await expect( + mTokenPermissioned.connect(from).transfer(to.address, 1), + ).revertedWithCustomError( + mTokenPermissioned, + acErrors.WMAC_BLACKLISTED().customErrorName, + ); + }); + + it('should fail: transfer when ERC20Pausable is paused', async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissioned, + mTokenPermissionedRoles, + } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + from.address, + ); + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + to.address, + ); + await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); + + await setErc20PausablePaused(mTokenPermissioned); + + await expect( + mTokenPermissioned.connect(from).transfer(to.address, 1), + ).revertedWith(ERC20_PAUSED_MSG); + }); + + it('should fail: mint when ERC20Pausable is paused', async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissioned, + mTokenPermissionedRoles, + } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); + + const to = regularAccounts[0]; + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + to.address, + ); + await setErc20PausablePaused(mTokenPermissioned); + + await mint({ tokenContract: mTokenPermissioned, owner }, to, 1, { + revertMessage: ERC20_PAUSED_MSG, + }); + }); + + it('should fail: burn when ERC20Pausable is paused', async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissioned, + mTokenPermissionedRoles, + } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); + + const holder = regularAccounts[0]; + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + holder.address, + ); + await mint({ tokenContract: mTokenPermissioned, owner }, holder, 1); + await setErc20PausablePaused(mTokenPermissioned); + + await burn({ tokenContract: mTokenPermissioned, owner }, holder, 1, { + revertMessage: ERC20_PAUSED_MSG, + }); + }); + + it('should fail: mint when receiver is not greenlisted', async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { owner, regularAccounts, mTokenPermissioned } = await loadFixture( + mTokenPermissionedFixture.bind(this, baseFixture), + ); + + await mint( + { tokenContract: mTokenPermissioned, owner }, + regularAccounts[0], + 1, + { revertCustomError: { customErrorName: 'NotGreenlisted' } }, + ); + }); + + it('transfer when both parties are greenlisted', async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissioned, + mTokenPermissionedRoles, + } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + from.address, + ); + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + to.address, + ); + await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); + + await expect(mTokenPermissioned.connect(from).transfer(to.address, 1)).not + .reverted; + expect(await mTokenPermissioned.balanceOf(to.address)).eq(1); + }); + + it('mint when receiver is greenlisted', async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissioned, + mTokenPermissionedRoles, + } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); + + const to = regularAccounts[0]; + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + to.address, + ); + + await mint( + { tokenContract: mTokenPermissioned, owner }, + to, + parseUnits('1'), + ); + }); + + it('burn without greenlist on holder', async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissioned, + mTokenPermissionedRoles, + } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); + + const holder = regularAccounts[0]; + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + holder.address, + ); + await mint({ tokenContract: mTokenPermissioned, owner }, holder, 1); + await accessControl.revokeRole( + mTokenPermissionedRoles.greenlisted, + holder.address, + ); + + await burn({ tokenContract: mTokenPermissioned, owner }, holder, 1); + }); + }); + + describe('transferFrom()', () => { + const greenlistComboCases: { + fromGreenlisted: boolean; + toGreenlisted: boolean; + callerGreenlisted: boolean; + expectSuccess: boolean; + }[] = [ + { + fromGreenlisted: true, + toGreenlisted: true, + callerGreenlisted: true, + expectSuccess: true, + }, + { + fromGreenlisted: true, + toGreenlisted: true, + callerGreenlisted: false, + expectSuccess: true, + }, + { + fromGreenlisted: false, + toGreenlisted: true, + callerGreenlisted: true, + expectSuccess: false, + }, + { + fromGreenlisted: false, + toGreenlisted: true, + callerGreenlisted: false, + expectSuccess: false, + }, + { + fromGreenlisted: false, + toGreenlisted: false, + callerGreenlisted: true, + expectSuccess: false, + }, + { + fromGreenlisted: false, + toGreenlisted: false, + callerGreenlisted: false, + expectSuccess: false, + }, + { + fromGreenlisted: true, + toGreenlisted: false, + callerGreenlisted: true, + expectSuccess: false, + }, + { + fromGreenlisted: true, + toGreenlisted: false, + callerGreenlisted: false, + expectSuccess: false, + }, + ]; + + greenlistComboCases.forEach( + ({ + fromGreenlisted, + toGreenlisted, + callerGreenlisted, + expectSuccess, + }) => { + const fromL = fromGreenlisted ? 'greenlisted' : 'not greenlisted'; + const toL = toGreenlisted ? 'greenlisted' : 'not greenlisted'; + const callerL = callerGreenlisted ? 'greenlisted' : 'not greenlisted'; + + it( + expectSuccess + ? `succeeds: from ${fromL}, to ${toL}, caller ${callerL}` + : `should fail: from ${fromL}, to ${toL}, caller ${callerL}`, + async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissioned, + mTokenPermissionedRoles, + } = await loadFixture( + mTokenPermissionedFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const caller = regularAccounts[1]; + const to = regularAccounts[2]; + const { greenlisted } = mTokenPermissionedRoles; + + await accessControl['grantRole(bytes32,address)']( + greenlisted, + from.address, + ); + await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); + await mTokenPermissioned.connect(from).approve(caller.address, 1); + + if (!fromGreenlisted) { + await accessControl.revokeRole(greenlisted, from.address); + } + if (toGreenlisted) { + await accessControl['grantRole(bytes32,address)']( + greenlisted, + to.address, + ); + } + if (callerGreenlisted) { + await accessControl['grantRole(bytes32,address)']( + greenlisted, + caller.address, + ); + } + + const tx = mTokenPermissioned + .connect(caller) + .transferFrom(from.address, to.address, 1); + + if (expectSuccess) { + await expect(tx).not.reverted; + expect(await mTokenPermissioned.balanceOf(to.address)).eq(1); + } else { + await expect(tx).revertedWithCustomError( + mTokenPermissioned, + 'NotGreenlisted', + ); + } + }, + ); + }, + ); + + it('should fail: transferFrom when from is blacklisted', async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissioned, + mTokenPermissionedRoles, + } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); + + const from = regularAccounts[0]; + const spender = regularAccounts[1]; + const to = regularAccounts[2]; + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + from.address, + ); + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + to.address, + ); + await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); + await blackList( + { + blacklistable: mTokenPermissioned, + accessControl, + owner, + }, + from, + ); + await mTokenPermissioned.connect(from).approve(spender.address, 1); + + await expect( + mTokenPermissioned + .connect(spender) + .transferFrom(from.address, to.address, 1), + ).revertedWithCustomError( + mTokenPermissioned, + acErrors.WMAC_BLACKLISTED().customErrorName, + ); + }); + + it('should fail: transferFrom when to is blacklisted', async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissioned, + mTokenPermissionedRoles, + } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); + + const from = regularAccounts[0]; + const spender = regularAccounts[1]; + const to = regularAccounts[2]; + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + from.address, + ); + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + to.address, + ); + await mint({ tokenContract: mTokenPermissioned, owner }, from, 1); + await blackList( + { + blacklistable: mTokenPermissioned, + accessControl, + owner, + }, + to, + ); + await mTokenPermissioned.connect(from).approve(spender.address, 1); + + await expect( + mTokenPermissioned + .connect(spender) + .transferFrom(from.address, to.address, 1), + ).revertedWithCustomError( + mTokenPermissioned, + acErrors.WMAC_BLACKLISTED().customErrorName, + ); + }); + }); + + describe('clawback()', () => { + it('should not fail when from address is not greenlisted', async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { + owner, + accessControl, + regularAccounts, + clawbackReceiver, + mTokenPermissioned, + mTokenPermissionedRoles, + } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); + + const holder = regularAccounts[0]; + const amount = parseUnits('1'); + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + holder.address, + ); + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + clawbackReceiver.address, + ); + await mint({ tokenContract: mTokenPermissioned, owner }, holder, amount); + await accessControl.revokeRole( + mTokenPermissionedRoles.greenlisted, + holder.address, + ); + + await clawbackTest( + { tokenContract: mTokenPermissioned, owner }, + amount, + holder, + ); + }); + + it('should fail: when clawbackReceiver is not greenlisted', async () => { + const baseFixture = await loadFixture(defaultDeploy); + const { + owner, + accessControl, + regularAccounts, + clawbackReceiver, + mTokenPermissioned, + mTokenPermissionedRoles, + } = await loadFixture(mTokenPermissionedFixture.bind(this, baseFixture)); + + const holder = regularAccounts[0]; + const amount = parseUnits('1'); + + await accessControl['grantRole(bytes32,address)']( + mTokenPermissionedRoles.greenlisted, + holder.address, + ); + await mint({ tokenContract: mTokenPermissioned, owner }, holder, amount); + await accessControl.revokeRole( + mTokenPermissionedRoles.greenlisted, + holder.address, + ); + await accessControl.revokeRole( + mTokenPermissionedRoles.greenlisted, + clawbackReceiver.address, + ); + + await clawbackTest( + { tokenContract: mTokenPermissioned, owner }, + amount, + holder, + { revertCustomError: { customErrorName: 'NotGreenlisted' } }, + ); + }); }); }); diff --git a/test/unit/suits/deposit-vault.suits.ts b/test/unit/suits/deposit-vault.suits.ts new file mode 100644 index 00000000..52f7a2f5 --- /dev/null +++ b/test/unit/suits/deposit-vault.suits.ts @@ -0,0 +1,11521 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { increase } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time'; +import { + days, + hours, +} from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time/duration'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { constants, Contract } from 'ethers'; +import { parseUnits } from 'ethers/lib/utils'; + +import { + baseInitParamsMv, + manageableVaultSuits, + mvInitializeParamCases, +} from './manageable-vault.suits'; + +import { encodeFnSelector } from '../../../helpers/utils'; +import { + DepositVaultTest, + DepositVaultTest__factory, + DepositVaultWithAaveTest, + DepositVaultWithMTokenTest, + DepositVaultWithUSTBTest, + DepositVaultWithMorphoTest, +} from '../../../typechain-types'; +import { + acErrors, + blackList, + greenList, + setupPermissionRole, +} from '../../common/ac.helpers'; +import { + approveBase18, + InitializeParamCase, + initializeParamsSuits, + InitializeParamsOpt, + mintToken, + pauseVault, + pauseVaultFn, + asyncForEach, +} from '../../common/common.helpers'; +import { + setMinGrowthApr, + setRoundDataGrowth, +} from '../../common/custom-feed-growth.helpers'; +import { setRoundData } from '../../common/data-feed.helpers'; +import { + setMaxSupplyCapTest, + setMaxAmountPerRequestTest, + approveRequestTest, + depositInstantTest, + depositRequestTest, + rejectRequestTest, + safeBulkApproveRequestTest, + expectedDepositHoldbackPartRateFromAvg, +} from '../../common/deposit-vault.helpers'; +import { DefaultFixture } from '../../common/fixtures'; +import { greenListEnable } from '../../common/greenlist.helpers'; +import { + addPaymentTokenTest, + removePaymentTokenTest, + setVariabilityToleranceTest, + changeTokenAllowanceTest, + setInstantFeeTest, + setInstantLimitConfigTest, + setMinAmountTest, + setMinMaxInstantFeeTest, + setMinAmountToDepositTest, + setMaxInstantShareTest, + setSequentialRequestProcessingTest, + setWaivedFeeAccountTest, + setMaxApproveRequestIdTest, +} from '../../common/manageable-vault.helpers'; +import { InitializerParamsDv } from '../../common/vault-initializer.helpers'; +import { sanctionUser } from '../../common/with-sanctions-list.helpers'; + +const APPROVE_FN_SELECTORS = [ + encodeFnSelector('approveRequest(uint256,uint256,bool)'), + encodeFnSelector('safeBulkApproveRequestAtSavedRate(uint256[])'), + encodeFnSelector('safeBulkApproveRequest(uint256[])'), + encodeFnSelector('safeBulkApproveRequest(uint256[],uint256)'), + encodeFnSelector('safeBulkApproveRequestAvgRate(uint256[])'), + encodeFnSelector('safeBulkApproveRequestAvgRate(uint256[],uint256)'), +] as const; + +let pauseManager: DefaultFixture['pauseManager']; +let owner: DefaultFixture['owner']; + +export const baseInitParamsDv = ( + fixture: DefaultFixture, +): InitializerParamsDv => ({ + ...baseInitParamsMv(fixture), +}); + +export const tokensReceiverSelfParamCase = < + TParams extends InitializerParamsDv, +>( + deployUninitialized: DepositVaultInitConfig['deployUninitialized'], +): InitializeParamCase => ({ + title: 'tokensReceiver is address(this)', + contract: deployUninitialized, + params: (_, contract) => ({ tokensReceiver: contract!.address }), + revertCustomError: { + customErrorName: 'InvalidAddress', + args: (_, contract) => [contract!.address], + }, +}); + +export type DepositVaultInitConfig< + TParams extends InitializerParamsDv = InitializerParamsDv, +> = { + deployUninitialized: ( + fixture: DefaultFixture, + ) => Contract | Promise; + initialize: ( + fixture: DefaultFixture, + params: Partial, + opt?: InitializeParamsOpt, + ) => Promise; + extraParamCases?: InitializeParamCase[]; +}; + +const pauseOtherDepositApproveFns = async ( + depositVault: Contract, + exceptSelector: (typeof APPROVE_FN_SELECTORS)[number], +) => { + await asyncForEach( + APPROVE_FN_SELECTORS, + async (selector) => { + if (selector === exceptSelector) { + return; + } + await pauseVaultFn({ pauseManager, owner }, depositVault, selector); + }, + true, + ); +}; +export const depositVaultSuits = ( + dvName: string, + dvFixture: () => Promise, + dvConfig: { + createNew: ( + owner: SignerWithAddress, + ) => Promise< + | DepositVaultTest + | DepositVaultWithAaveTest + | DepositVaultWithMTokenTest + | DepositVaultWithUSTBTest + | DepositVaultWithMorphoTest + >; + key: + | 'depositVault' + | 'depositVaultWithUSTB' + | 'depositVaultWithMToken' + | 'depositVaultWithAave' + | 'depositVaultWithMorpho'; + }, + deploymentAdditionalChecks: (fixtureRes: DefaultFixture) => Promise, + initConfig: DepositVaultInitConfig, + otherTests: (fixture: () => Promise) => void, +) => { + const loadDvFixture = async () => { + const fixture = await loadFixture(dvFixture); + ({ pauseManager, owner } = fixture); + + const { createNew, key } = dvConfig; + return { + ...fixture, + originalDepositVault: fixture.depositVault, + depositVault: DepositVaultTest__factory.connect( + fixture[key].address, + fixture.owner, + ), + createNew: async () => { + const dv = await createNew(fixture.owner); + return DepositVaultTest__factory.connect(dv.address, fixture.owner); + }, + }; + }; + + describe(dvName, function () { + manageableVaultSuits(loadDvFixture, dvConfig, async (fixture) => { + const { depositVault, roles } = fixture; + expect(await depositVault.contractAdminRole()).eq( + roles.tokenRoles.mTBILL.depositVaultAdmin, + ); + }); + + it('deployment', async () => { + const fixture = await loadDvFixture(); + const { depositVault } = fixture; + + expect(await depositVault.minMTokenAmountForFirstDeposit()).eq('0'); + + expect(await depositVault.maxSupplyCap()).eq(constants.MaxUint256); + + await deploymentAdditionalChecks({ + ...fixture, + depositVault: fixture.originalDepositVault as DepositVaultTest, + }); + }); + + describe('initialization', () => { + initializeParamsSuits( + [ + ...mvInitializeParamCases, + tokensReceiverSelfParamCase(initConfig.deployUninitialized), + ...(initConfig.extraParamCases ?? []), + ], + loadDvFixture, + initConfig.initialize, + ); + }); + + describe('common', () => { + describe('getEffectiveMTokenSupply()', () => { + it('returns mToken totalSupply when upcomingSupply is zero', async () => { + const { depositVault, mTBILL } = await loadDvFixture(); + + expect(await depositVault.upcomingSupply()).eq(0); + expect(await depositVault.getEffectiveMTokenSupply()).eq( + await mTBILL.totalSupply(), + ); + }); + + it('returns totalSupply plus upcomingSupply from pending deposit requests', async () => { + const { + depositVault, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, regularAccounts[0], 1000); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 1000, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + const supplyBefore = await mTBILL.totalSupply(); + const upcomingBefore = await depositVault.upcomingSupply(); + expect(await depositVault.getEffectiveMTokenSupply()).eq( + supplyBefore.add(upcomingBefore), + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 100, + { from: regularAccounts[0] }, + ); + + const upcomingAfter = await depositVault.upcomingSupply(); + expect(upcomingAfter).gt(upcomingBefore); + expect(await depositVault.getEffectiveMTokenSupply()).eq( + supplyBefore.add(upcomingAfter), + ); + }); + }); + + describe('setMinMTokenAmountForFirstDeposit()', () => { + it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { owner, depositVault, regularAccounts } = + await loadDvFixture(); + + await setMinAmountToDepositTest({ depositVault, owner }, 1.1, { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }); + }); + + it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { owner, depositVault } = await loadDvFixture(); + await setMinAmountToDepositTest({ depositVault, owner }, 1.1); + }); + + it('should fail: when function is paused', async () => { + const { owner, depositVault } = await loadDvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + depositVault, + encodeFnSelector('setMinMTokenAmountForFirstDeposit(uint256)'), + ); + + await setMinAmountToDepositTest({ depositVault, owner }, 1.1, { + revertCustomError: { + customErrorName: 'Paused', + }, + }); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, depositVault, regularAccounts } = + await loadDvFixture(); + + const contractAdminRole = await depositVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + depositVault.address, + 'setMinMTokenAmountForFirstDeposit(uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole( + contractAdminRole, + regularAccounts[0].address, + ), + ).eq(false); + + await setMinAmountToDepositTest({ depositVault, owner }, 2.2, { + from: regularAccounts[0], + }); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { accessControl, owner, depositVault, regularAccounts, roles } = + await loadDvFixture(); + + const contractAdminRole = await depositVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + depositVault.address, + 'setMinMTokenAmountForFirstDeposit(uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await setMinAmountToDepositTest({ depositVault, owner }, 2.2, { + from: regularAccounts[0], + }); + }); + }); + + describe('setMaxSupplyCap()', () => { + it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { owner, depositVault, regularAccounts } = + await loadDvFixture(); + + await setMaxSupplyCapTest({ depositVault, owner }, 1.1, { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }); + }); + + it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { owner, depositVault } = await loadDvFixture(); + await setMaxSupplyCapTest({ depositVault, owner }, 1.1); + }); + + it('should fail: when function is paused', async () => { + const { owner, depositVault } = await loadDvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + depositVault, + encodeFnSelector('setMaxSupplyCap(uint256)'), + ); + + await setMaxSupplyCapTest({ depositVault, owner }, 1.1, { + revertCustomError: { + customErrorName: 'Paused', + }, + }); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, depositVault, regularAccounts } = + await loadDvFixture(); + + const contractAdminRole = await depositVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + depositVault.address, + 'setMaxSupplyCap(uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole( + contractAdminRole, + regularAccounts[0].address, + ), + ).eq(false); + + await setMaxSupplyCapTest({ depositVault, owner }, 2.2, { + from: regularAccounts[0], + }); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { accessControl, owner, depositVault, regularAccounts, roles } = + await loadDvFixture(); + + const contractAdminRole = await depositVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + depositVault.address, + 'setMaxSupplyCap(uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await setMaxSupplyCapTest({ depositVault, owner }, 2.2, { + from: regularAccounts[0], + }); + }); + }); + + describe('setMaxAmountPerRequest()', () => { + it('should fail: call from address without DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { owner, depositVault, regularAccounts } = + await loadDvFixture(); + + await setMaxAmountPerRequestTest({ depositVault, owner }, 100, { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }); + }); + + it('call from address with DEPOSIT_VAULT_ADMIN_ROLE role', async () => { + const { owner, depositVault } = await loadDvFixture(); + await setMaxAmountPerRequestTest({ depositVault, owner }, 100); + }); + + it('should fail: when function is paused', async () => { + const { owner, depositVault } = await loadDvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + depositVault, + encodeFnSelector('setMaxAmountPerRequest(uint256)'), + ); + + await setMaxAmountPerRequestTest({ depositVault, owner }, 100, { + revertCustomError: { + customErrorName: 'Paused', + }, + }); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, depositVault, regularAccounts } = + await loadDvFixture(); + + const contractAdminRole = await depositVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + depositVault.address, + 'setMaxAmountPerRequest(uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole( + contractAdminRole, + regularAccounts[0].address, + ), + ).eq(false); + + await setMaxAmountPerRequestTest({ depositVault, owner }, 200, { + from: regularAccounts[0], + }); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { accessControl, owner, depositVault, regularAccounts, roles } = + await loadDvFixture(); + + const contractAdminRole = await depositVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + depositVault.address, + 'setMaxAmountPerRequest(uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await setMaxAmountPerRequestTest({ depositVault, owner }, 200, { + from: regularAccounts[0], + }); + }); + }); + + describe('depositInstant()', async () => { + it('should fail: if mint exceed allowance', async () => { + const { + depositVault, + stableCoins, + owner, + dataFeed, + mTBILL, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, owner, 100000); + await approveBase18(owner, stableCoins.dai, depositVault, 100000); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await changeTokenAllowanceTest( + { vault: depositVault, owner }, + stableCoins.dai.address, + 100, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 99_999, + { + revertCustomError: { + customErrorName: 'AllowanceExceeded', + }, + }, + ); + }); + + it('should decrease allowance if allowance < UINT_MAX', async () => { + const { + depositVault, + stableCoins, + owner, + dataFeed, + mTBILL, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, owner, 100000); + await approveBase18(owner, stableCoins.dai, depositVault, 100000); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await changeTokenAllowanceTest( + { vault: depositVault, owner }, + stableCoins.dai.address, + parseUnits('1000'), + ); + + const tokenConfigBefore = await depositVault.tokensConfig( + stableCoins.dai.address, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 999, + ); + + const tokenConfigAfter = await depositVault.tokensConfig( + stableCoins.dai.address, + ); + + expect( + tokenConfigBefore.allowance.sub(tokenConfigAfter.allowance), + ).eq(parseUnits('999')); + }); + + it('should not decrease allowance if allowance = UINT_MAX', async () => { + const { + depositVault, + stableCoins, + owner, + dataFeed, + mTBILL, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, owner, 100000); + await approveBase18(owner, stableCoins.dai, depositVault, 100000); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await changeTokenAllowanceTest( + { vault: depositVault, owner }, + stableCoins.dai.address, + constants.MaxUint256, + ); + + const tokenConfigBefore = await depositVault.tokensConfig( + stableCoins.dai.address, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 999, + ); + + const tokenConfigAfter = await depositVault.tokensConfig( + stableCoins.dai.address, + ); + + expect(tokenConfigBefore.allowance).eq(tokenConfigAfter.allowance); + }); + + it('should fail: when there is no token in vault', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + revertCustomError: { + customErrorName: 'UnknownPaymentToken', + }, + }, + ); + }); + + it('should fail: when trying to deposit 0 amount', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 0, + { + revertCustomError: { + customErrorName: 'InvalidAmount', + }, + }, + ); + }); + + it('should fail: when function paused', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + const selector = encodeFnSelector( + 'depositInstant(address,uint256,uint256,bytes32)', + ); + await pauseVaultFn({ pauseManager, owner }, depositVault, selector); + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: when rounding is invalid', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100.0000000001, + { + revertCustomError: { + customErrorName: 'InvalidRounding', + }, + }, + ); + }); + + it('should fail: call with insufficient allowance', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: insufficient allowance', + }, + ); + }); + + it('should fail: call with insufficient balance', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('should fail: dataFeed rate 0 ', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mockedAggregator, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await approveBase18(owner, stableCoins.dai, depositVault, 10); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await mintToken(stableCoins.dai, owner, 100_000); + await setRoundData({ mockedAggregator }, 0); + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + revertMessage: 'DF: feed is deprecated', + }, + ); + }); + + it('should fail: call for amount < minAmountToDepositTest', async () => { + const { + depositVault, + mockedAggregator, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await mintToken(stableCoins.dai, owner, 100_000); + await approveBase18(owner, stableCoins.dai, depositVault, 100_000); + + await setMinAmountToDepositTest({ depositVault, owner }, 100_000); + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + 150_000, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 99_999, + { + revertCustomError: { + customErrorName: 'LessThanMinAmountFirstDeposit', + }, + }, + ); + }); + + it('should fail: call for amount < minAmount', async () => { + const { + depositVault, + mockedAggregator, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await setMinAmountTest({ vault: depositVault, owner }, 100); + + await mintToken(stableCoins.dai, owner, 100_000); + await approveBase18(owner, stableCoins.dai, depositVault, 100_000); + + await setMinAmountToDepositTest({ depositVault, owner }, 100_000); + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + 150_000, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 99, + { + revertCustomError: { + customErrorName: 'AmountLessThanMin', + }, + }, + ); + }); + + it('should fail: if exceed allowance of deposit for token', async () => { + const { + depositVault, + mockedAggregator, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + + await mintToken(stableCoins.dai, owner, 100_000); + await changeTokenAllowanceTest( + { vault: depositVault, owner }, + stableCoins.dai.address, + 100, + ); + await approveBase18(owner, stableCoins.dai, depositVault, 100_000); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 99_999, + { + revertCustomError: { + customErrorName: 'AllowanceExceeded', + }, + }, + ); + }); + + it('should fail: if mint limit exceeded', async () => { + const { + depositVault, + mockedAggregator, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + + await mintToken(stableCoins.dai, owner, 100_000); + await setInstantLimitConfigTest({ vault: depositVault, owner }, 1000); + + await approveBase18(owner, stableCoins.dai, depositVault, 100_000); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 99_999, + { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + }, + }, + ); + }); + + it('should fail: MV: invalid instant fee when instant fee below min', async () => { + const { + depositVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, owner, 1_000_000); + + await setInstantFeeTest({ vault: depositVault, owner }, 100); + await setMinMaxInstantFeeTest( + { vault: depositVault, owner }, + 200, + 10_000, + ); + + await approveBase18(owner, stableCoins.dai, depositVault, 1_000_000); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'InstantFeeOutOfBounds', + }, + }, + ); + }); + + it('should fail: MV: invalid instant fee when instant fee above max', async () => { + const { + depositVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, owner, 1_000_000); + + await setInstantFeeTest({ vault: depositVault, owner }, 5000); + await setMinMaxInstantFeeTest( + { vault: depositVault, owner }, + 0, + 2000, + ); + + await approveBase18(owner, stableCoins.dai, depositVault, 1_000_000); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'InstantFeeOutOfBounds', + }, + }, + ); + }); + + describe('depositInstant() sliding rate limit (RateLimitLibrary)', () => { + const setupDepositInstantRateLimitFixture = async () => { + const fixture = await loadDvFixture(); + const { + depositVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + } = fixture; + + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, owner, 1_000_000); + await setMinAmountTest({ vault: depositVault, owner }, 0); + await approveBase18( + owner, + stableCoins.dai, + depositVault, + 1_000_000, + ); + + return fixture; + }; + + it('10h window: full consume, after 1h restores ~10% and two deposits use it', async () => { + const { + depositVault, + owner, + mTBILL, + stableCoins, + mTokenToUsdDataFeed, + } = await setupDepositInstantRateLimitFixture(); + + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: hours(10), limit: parseUnits('1000') }, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1000, + ); + + await increase(hours(1)); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('1d window: after 80% consumed and limit halved, deposit fails', async () => { + const { + depositVault, + owner, + mTBILL, + stableCoins, + mTokenToUsdDataFeed, + } = await setupDepositInstantRateLimitFixture(); + + const initialLimit = parseUnits('1000'); + + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: days(1), limit: initialLimit }, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 800, + ); + + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: days(1), limit: initialLimit.div(2) }, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + }, + }, + ); + }); + + it('1d window: after limit halved, wait 12h and deposit small amount', async () => { + const { + depositVault, + owner, + mTBILL, + stableCoins, + mTokenToUsdDataFeed, + } = await setupDepositInstantRateLimitFixture(); + + const initialLimit = parseUnits('1000'); + + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: days(1), limit: initialLimit }, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 800, + ); + + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: days(1), limit: initialLimit.div(2) }, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + }, + }, + ); + + await increase(hours(18)); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + ); + }); + + it('multiple windows active at the same time', async () => { + const { + depositVault, + owner, + mTBILL, + stableCoins, + mTokenToUsdDataFeed, + } = await setupDepositInstantRateLimitFixture(); + + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: hours(1), limit: parseUnits('100') }, + ); + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: hours(6), limit: parseUnits('500') }, + ); + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: days(1), limit: parseUnits('10000') }, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + }, + }, + ); + + await increase(hours(1)); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + }); + }); + + describe('depositInstant() multiple instant limits', () => { + it('two windows (12h and 1d): deposit succeeds when under both', async () => { + const { + depositVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, owner, 1_000_000); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: hours(12), limit: parseUnits('1000') }, + ); + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: days(1), limit: parseUnits('2000') }, + ); + + await approveBase18( + owner, + stableCoins.dai, + depositVault, + 1_000_000, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 500, + ); + }); + + it('two windows: deposit fails when one window (12h) is filled', async () => { + const { + depositVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, owner, 1_000_000); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: hours(12), limit: parseUnits('100') }, + ); + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: days(1), limit: parseUnits('10000') }, + ); + + await approveBase18( + owner, + stableCoins.dai, + depositVault, + 1_000_000, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 60, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + }, + }, + ); + }); + + it('two windows: 12h epoch resets after 12h, deposit succeeds again', async () => { + const { + depositVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, owner, 1_000_000); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: hours(12), limit: parseUnits('100') }, + ); + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: days(1), limit: parseUnits('10000') }, + ); + + await approveBase18( + owner, + stableCoins.dai, + depositVault, + 1_000_000, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await increase(hours(12)); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 10, + ); + }); + }); + + it('should fail: if min receive amount greater then actual', async () => { + const { + depositVault, + mockedAggregator, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + + await mintToken(stableCoins.dai, owner, 100_000); + + await approveBase18(owner, stableCoins.dai, depositVault, 100_000); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + minAmount: parseUnits('100000'), + }, + stableCoins.dai, + 99_999, + { + revertCustomError: { + customErrorName: 'SlippageExceeded', + }, + }, + ); + }); + + it('should fail: if some fee = 100%', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 10000, + true, + ); + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'InvalidAmount', + }, + }, + ); + + await removePaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest({ vault: depositVault, owner }, 10000); + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'InvalidAmount', + }, + }, + ); + }); + + it('should fail: greenlist enabled and user not in greenlist ', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await depositVault.setGreenlistEnable(true); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + revertCustomError: { + customErrorName: 'NotGreenlisted', + }, + }, + ); + }); + + it('should fail: user in blacklist ', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + blackListableTester, + accessControl, + regularAccounts, + } = await loadDvFixture(); + + await blackList( + { blacklistable: blackListableTester, accessControl, owner }, + regularAccounts[0], + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_BLACKLISTED, + }, + ); + }); + + it('should fail: user in sanctions list', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + mockedSanctionsList, + } = await loadDvFixture(); + + await sanctionUser( + { sanctionsList: mockedSanctionsList }, + regularAccounts[0], + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Sanctioned', + }, + }, + ); + }); + + it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + greenListableTester, + accessControl, + customRecipient, + } = await loadDvFixture(); + + await greenList( + { greenlistable: greenListableTester, accessControl, owner }, + owner, + ); + + await depositVault.setGreenlistEnable(true); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 1, + { + revertCustomError: { + customErrorName: 'NotGreenlisted', + }, + }, + ); + }); + + it('should fail: recipient in blacklist (custom recipient overload)', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + blackListableTester, + accessControl, + regularAccounts, + customRecipient, + } = await loadDvFixture(); + + await blackList( + { blacklistable: blackListableTester, accessControl, owner }, + customRecipient, + ); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_BLACKLISTED, + }, + ); + }); + + it('should fail: recipient in sanctions list (custom recipient overload)', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + mockedSanctionsList, + customRecipient, + } = await loadDvFixture(); + + await sanctionUser( + { sanctionsList: mockedSanctionsList }, + customRecipient, + ); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Sanctioned', + }, + }, + ); + }); + + it('should fail: when function paused (custom recipient overload)', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + customRecipient, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + const selector = encodeFnSelector( + 'depositInstant(address,uint256,uint256,bytes32,address)', + ); + await pauseVaultFn({ pauseManager, owner }, depositVault, selector); + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: when 0 supply cap is left', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + customRecipient, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setMaxSupplyCapTest({ depositVault, owner }, 99); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 99, + { + from: regularAccounts[0], + }, + ); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'SupplyCapExceeded', + }, + }, + ); + }); + + it('should fail: when 10 supply cap is left and try to mint 11', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + customRecipient, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, regularAccounts[0], 101); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 101, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setMaxSupplyCapTest({ depositVault, owner }, 100); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 90, + { + from: regularAccounts[0], + }, + ); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 11, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'SupplyCapExceeded', + }, + }, + ); + }); + + it('when 10 supply cap is left and try to mint 10', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + customRecipient, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setMaxSupplyCapTest({ depositVault, owner }, 100); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 90, + { + from: regularAccounts[0], + }, + ); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 10, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 DAI, greenlist enabled and user in greenlist ', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + greenListableTester, + mTokenToUsdDataFeed, + accessControl, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await depositVault.setGreenlistEnable(true); + + await greenList( + { greenlistable: greenListableTester, accessControl, owner }, + regularAccounts[0], + ); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 DAI when 10% growth is applied', async () => { + const { + owner, + depositVault, + customFeedGrowth, + stableCoins, + mTBILL, + greenListableTester, + mTokenToUsdDataFeed, + accessControl, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await depositVault.setGreenlistEnable(true); + + await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); + await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, 10); + + await greenList( + { greenlistable: greenListableTester, accessControl, owner }, + regularAccounts[0], + ); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + expectedMintAmount: parseUnits('98.999684191007430686', 18), + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 DAI when -10% growth is applied', async () => { + const { + owner, + depositVault, + customFeedGrowth, + stableCoins, + mTBILL, + greenListableTester, + mTokenToUsdDataFeed, + accessControl, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await depositVault.setGreenlistEnable(true); + + await setMinGrowthApr({ owner, customFeedGrowth }, -10); + await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); + await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, -10); + + await greenList( + { greenlistable: greenListableTester, accessControl, owner }, + regularAccounts[0], + ); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + expectedMintAmount: parseUnits('99.000315811007437113', 18), + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 DAI, greenlist enabled and user in greenlist, tokenIn not stablecoin', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + greenListableTester, + mTokenToUsdDataFeed, + accessControl, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await depositVault.setGreenlistEnable(true); + + await greenList( + { greenlistable: greenListableTester, accessControl, owner }, + regularAccounts[0], + ); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + false, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 DAI, when price of stable is 1.03$ and mToken price is 5$', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('deposit 100 DAI, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('deposit 100 DAI, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await depositVault.freeFromMinAmount(owner.address, true); + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('deposit 100 DAI, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setWaivedFeeAccountTest( + { vault: depositVault, owner }, + owner.address, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + waivedFee: true, + }, + stableCoins.dai, + 100, + ); + }); + + it('deposit 100 DAI (custom recipient overload)', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + greenListableTester, + mTokenToUsdDataFeed, + accessControl, + regularAccounts, + dataFeed, + customRecipient, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 DAI when recipient == msg.sender (custom recipient overload)', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient: regularAccounts[0], + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 DAI when other overload of depositInstant is paused (custom recipient overload)', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + customRecipient, + } = await loadDvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + depositVault, + encodeFnSelector('depositInstant(address,uint256,uint256,bytes32)'), + ); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 DAI when other overload of depositInstant is paused', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + depositVault, + encodeFnSelector( + 'depositInstant(address,uint256,uint256,bytes32,address)', + ), + ); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + }); + + describe('depositRequest()', async () => { + describe('holdback', () => { + it('when 40% instant and 60% holdback', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 40_00, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when 90% instant and 10% holdback', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 90_00, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when 0% instant and 100% holdback', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 0, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when 50% instant and 50% holdback and request recipient is different from msg.sender', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + customRecipient: regularAccounts[1], + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when 50% instant and 50% holdback and instant recipient is different from msg.sender', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + customRecipientInstant: regularAccounts[2], + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when 50% instant and 50% holdback and request and instant recipients are different from msg.sender and from each other', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + customRecipient: regularAccounts[1], + customRecipientInstant: regularAccounts[2], + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when 100% instant and 0% holdback', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 100_00, + customRecipient: regularAccounts[1], + customRecipientInstant: regularAccounts[2], + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'InvalidAmount', + }, + }, + ); + }); + + it('should fail: when instant share exceeds max instant share', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await setMaxInstantShareTest({ vault: depositVault, owner }, 90_00); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 95_00, + customRecipient: regularAccounts[1], + customRecipientInstant: regularAccounts[2], + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'InstantShareTooHigh', + }, + }, + ); + }); + }); + + it('should fail: when estimated mint amount exceeds max amount per request', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + await setMaxAmountPerRequestTest({ depositVault, owner }, 1); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'MaxAmountPerRequestExceeded', + }, + }, + ); + }); + + it('should fail: if mint exceed allowance', async () => { + const { + depositVault, + stableCoins, + owner, + dataFeed, + mTBILL, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, owner, 100000); + await approveBase18(owner, stableCoins.dai, depositVault, 100000); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await changeTokenAllowanceTest( + { vault: depositVault, owner }, + stableCoins.dai.address, + 100, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 99_999, + { + revertCustomError: { + customErrorName: 'AllowanceExceeded', + }, + }, + ); + }); + + it('should fail: when there is no token in vault', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + revertCustomError: { + customErrorName: 'UnknownPaymentToken', + }, + }, + ); + }); + + it('should fail: when trying to deposit 0 amount', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 0, + { + revertCustomError: { + customErrorName: 'InvalidAmount', + }, + }, + ); + }); + + it('should fail: MV: invalid instant fee when instant fee below min', async () => { + const { + depositVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, owner, 1_000_000); + + await setInstantFeeTest({ vault: depositVault, owner }, 100); + await setMinMaxInstantFeeTest( + { vault: depositVault, owner }, + 200, + 10_000, + ); + + await approveBase18(owner, stableCoins.dai, depositVault, 1_000_000); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'InstantFeeOutOfBounds', + }, + }, + ); + }); + + it('should fail: MV: invalid instant fee when instant fee above max', async () => { + const { + depositVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, owner, 1_000_000); + + await setInstantFeeTest({ vault: depositVault, owner }, 5000); + await setMinMaxInstantFeeTest( + { vault: depositVault, owner }, + 0, + 2000, + ); + + await approveBase18(owner, stableCoins.dai, depositVault, 1_000_000); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'InstantFeeOutOfBounds', + }, + }, + ); + }); + + it('instant limit configs are not applied', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + mockedAggregator, + } = await loadDvFixture(); + + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: hours(12), limit: parseUnits('100') }, + ); + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + { window: days(1), limit: parseUnits('100') }, + ); + + await mintToken(stableCoins.dai, regularAccounts[0], 500); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 500, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 500, + { from: regularAccounts[0] }, + ); + }); + + it('should fail: when function paused', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + const selector = encodeFnSelector( + 'depositRequest(address,uint256,bytes32)', + ); + await pauseVaultFn({ pauseManager, owner }, depositVault, selector); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: when function paused (custom recipient overload)', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + customRecipient, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + const selector = encodeFnSelector( + 'depositRequest(address,uint256,bytes32,address,uint256,uint256,address)', + ); + await pauseVaultFn({ pauseManager, owner }, depositVault, selector); + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: when rounding is invalid', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100.0000000001, + { + revertCustomError: { + customErrorName: 'InvalidRounding', + }, + }, + ); + }); + + it('should fail: call with insufficient allowance', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: insufficient allowance', + }, + ); + }); + + it('should fail: call with insufficient balance', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('should fail: dataFeed rate 0 ', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mockedAggregator, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await approveBase18(owner, stableCoins.dai, depositVault, 10); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await mintToken(stableCoins.dai, owner, 100_000); + await setRoundData({ mockedAggregator }, 0); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + revertMessage: 'DF: feed is deprecated', + }, + ); + }); + + it('should fail: call for amount < minAmountToDepositTest', async () => { + const { + depositVault, + mockedAggregator, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await mintToken(stableCoins.dai, owner, 100_000); + await approveBase18(owner, stableCoins.dai, depositVault, 100_000); + + await setMinAmountToDepositTest({ depositVault, owner }, 100_000); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 99_999, + { + revertCustomError: { + customErrorName: 'LessThanMinAmountFirstDeposit', + }, + }, + ); + }); + + it('should fail: if exceed allowance of deposit for token', async () => { + const { + depositVault, + mockedAggregator, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + + await mintToken(stableCoins.dai, owner, 100_000); + await changeTokenAllowanceTest( + { vault: depositVault, owner }, + stableCoins.dai.address, + 100, + ); + await approveBase18(owner, stableCoins.dai, depositVault, 100_000); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 99_999, + { + revertCustomError: { + customErrorName: 'AllowanceExceeded', + }, + }, + ); + }); + + it('should fail: if token fee = 100%', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 10000, + true, + ); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'InvalidAmount', + }, + }, + ); + }); + + it('should fail: greenlist enabled and user not in greenlist ', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await depositVault.setGreenlistEnable(true); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + revertCustomError: { + customErrorName: 'NotGreenlisted', + }, + }, + ); + }); + + it('should fail: user in blacklist ', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + blackListableTester, + accessControl, + regularAccounts, + } = await loadDvFixture(); + + await blackList( + { blacklistable: blackListableTester, accessControl, owner }, + regularAccounts[0], + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_BLACKLISTED, + }, + ); + }); + + it('should fail: user in sanctionlist ', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + mockedSanctionsList, + } = await loadDvFixture(); + + await sanctionUser( + { sanctionsList: mockedSanctionsList }, + regularAccounts[0], + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Sanctioned', + }, + }, + ); + }); + + it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + greenListableTester, + accessControl, + customRecipient, + } = await loadDvFixture(); + + await depositVault.setGreenlistEnable(true); + + await greenList( + { greenlistable: greenListableTester, accessControl, owner }, + owner, + ); + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 1, + { + revertCustomError: { + customErrorName: 'NotGreenlisted', + }, + }, + ); + }); + + it('should fail: recipient in blacklist (custom recipient overload)', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + blackListableTester, + accessControl, + regularAccounts, + customRecipient, + } = await loadDvFixture(); + + await blackList( + { blacklistable: blackListableTester, accessControl, owner }, + customRecipient, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_BLACKLISTED, + }, + ); + }); + + it('should fail: recipient in sanctionlist (custom recipient overload)', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + mockedSanctionsList, + customRecipient, + } = await loadDvFixture(); + + await sanctionUser( + { sanctionsList: mockedSanctionsList }, + customRecipient, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Sanctioned', + }, + }, + ); + }); + + it('deposit 100 DAI, greenlist enabled and user in greenlist ', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + greenListableTester, + mTokenToUsdDataFeed, + accessControl, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await greenListEnable( + { greenlistable: greenListableTester, owner }, + true, + ); + + await greenList( + { greenlistable: greenListableTester, accessControl, owner }, + regularAccounts[0], + ); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 DAI when 10% growth is applied', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + greenListableTester, + mTokenToUsdDataFeed, + accessControl, + regularAccounts, + dataFeed, + customFeedGrowth, + } = await loadDvFixture(); + + await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); + await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, 10); + + await greenListEnable( + { greenlistable: greenListableTester, owner }, + true, + ); + + await greenList( + { greenlistable: greenListableTester, accessControl, owner }, + regularAccounts[0], + ); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 DAI when -10% growth is applied', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + greenListableTester, + mTokenToUsdDataFeed, + accessControl, + regularAccounts, + dataFeed, + customFeedGrowth, + } = await loadDvFixture(); + + await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); + await setMinGrowthApr({ owner, customFeedGrowth }, -10); + await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, -10); + + await greenListEnable( + { greenlistable: greenListableTester, owner }, + true, + ); + + await greenList( + { greenlistable: greenListableTester, accessControl, owner }, + regularAccounts[0], + ); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit request with 100 DAI, when price of stable is 1.03$ and mToken price is 5$', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('deposit request with 100 DAI, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('deposit request with 100 DAI, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await depositVault.freeFromMinAmount(owner.address, true); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('deposit request with 100 DAI, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setWaivedFeeAccountTest( + { vault: depositVault, owner }, + owner.address, + true, + ); + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + waivedFee: true, + }, + stableCoins.dai, + 100, + ); + }); + + it('deposit 100 (custom recipient overload)', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + customRecipient, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 when recipient == msg.sender (custom recipient overload)', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient: regularAccounts[0], + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 DAI when other overload of depositRequest is paused (custom recipient overload)', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + customRecipient, + } = await loadDvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + depositVault, + encodeFnSelector('depositRequest(address,uint256,bytes32)'), + ); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('deposit 100 DAI when other overload of depositRequest is paused', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadDvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + depositVault, + encodeFnSelector( + 'depositRequest(address,uint256,bytes32,address,uint256,uint256,address)', + ), + ); + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('should fail: cant deposit if effective supply is > max cap', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + customRecipient, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, regularAccounts[0], 101); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 101, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setMaxSupplyCapTest({ depositVault, owner }, 100); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 99, + { + from: regularAccounts[0], + }, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 2, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'SupplyCapExceeded', + }, + }, + ); + }); + + it('should fail: cant deposit if effective supply is > max cap when pending requests fill cap', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, regularAccounts[0], 120); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 120, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setMaxSupplyCapTest({ depositVault, owner }, 100); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 60, + { + from: regularAccounts[0], + }, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'SupplyCapExceeded', + }, + }, + ); + }); + }); + + describe('approveRequest()', async () => { + it('should fail: when request id exceeds maxApproveRequestId', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await setMaxApproveRequestIdTest({ vault: depositVault, owner }, 0); + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 1, + parseUnits('5'), + { + revertCustomError: { + customErrorName: 'RequestIdTooHigh', + args: [1, 0], + }, + }, + ); + }); + + describe('isAvgRate=false', async () => { + it('should fail: call from address without vault admin role', async () => { + const { + depositVault, + regularAccounts, + mTokenToUsdDataFeed, + mTBILL, + } = await loadDvFixture(); + await approveRequestTest( + { + depositVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + }, + 1, + parseUnits('5'), + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 1, + parseUnits('5'), + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + describe('request addresses access', () => { + const setupPendingDepositRequest = async ( + fixture: Awaited>, + opts?: { + customRecipient?: SignerWithAddress; + customClaimer?: SignerWithAddress; + }, + ) => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = fixture; + + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 5, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient: opts?.customRecipient ?? regularAccounts[0], + ...(opts?.customClaimer + ? { + customClaimer: opts.customClaimer, + instantShare: 0, + } + : {}), + }, + stableCoins.dai, + 100, + { from: regularAccounts[0] }, + ); + }; + + it('should fail: approve request when recipient got blacklisted', async () => { + const fixture = await loadDvFixture(); + const { + owner, + depositVault, + mTBILL, + mTokenToUsdDataFeed, + blackListableTester, + accessControl, + regularAccounts, + } = fixture; + await setupPendingDepositRequest(fixture); + + await blackList( + { blacklistable: blackListableTester, accessControl, owner }, + regularAccounts[0], + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('5'), + { revertCustomError: acErrors.WMAC_BLACKLISTED }, + ); + }); + + it('should fail: approve request when recipient got ungreenlisted when greenlist enable flag is true', async () => { + const fixture = await loadDvFixture(); + const { owner, depositVault, mTBILL, mTokenToUsdDataFeed } = + fixture; + await setupPendingDepositRequest(fixture); + + await depositVault.setGreenlistEnable(true); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('5'), + { + revertCustomError: { + customErrorName: 'NotGreenlisted', + }, + }, + ); + }); + + it('should fail: approve request when recipient got sanction listed', async () => { + const fixture = await loadDvFixture(); + const { + owner, + depositVault, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + mockedSanctionsList, + } = fixture; + await setupPendingDepositRequest(fixture); + + await sanctionUser( + { sanctionsList: mockedSanctionsList }, + regularAccounts[0], + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('5'), + { + revertCustomError: { + customErrorName: 'Sanctioned', + }, + }, + ); + }); + }); + + it('should fail: request already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('5'), + ); + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('5'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('approve request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('5'), + ); + }); + + it('should succeed when other approve entrypoints are paused', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await pauseOtherDepositApproveFns( + depositVault, + encodeFnSelector('approveRequest(uint256,uint256,bool)'), + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('5'), + ); + }); + + it('approve request should decrease pending supply', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMaxSupplyCapTest({ depositVault, owner }, 100); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('1'), + ); + }); + + it('should fail: when after approval supply exceeds max cap', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMaxSupplyCapTest({ depositVault, owner }, 100); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 90, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 10, + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('0.99'), + { + revertCustomError: { + customErrorName: 'SupplyCapExceeded', + }, + }, + ); + }); + + it('should approve request in non sequential order when sequentialRequestProcessing is disabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 1, + parseUnits('1'), + ); + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('1'), + ); + }); + + it('should fail: approve request in non sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('1'), + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 2, + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'InvalidRequestSequence', + args: [2, 1], + }, + }, + ); + }); + + it('should approve request id 0 and then id 1 when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('1'), + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 1, + parseUnits('1'), + ); + }); + + it('should enforce fifo across separate transactions when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 500); + await approveBase18(owner, stableCoins.dai, depositVault, 500); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await asyncForEach( + Array.from({ length: 9 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + }, + true, + ); + + await asyncForEach( + [0, 1, 2], + async (requestId) => { + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('1'), + ); + }, + true, + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 3, + parseUnits('1'), + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 5, + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'InvalidRequestSequence', + args: [5, 4], + }, + }, + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 4, + parseUnits('1'), + ); + + await asyncForEach( + [6, 7, 8], + async (requestId) => { + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'InvalidRequestSequence', + args: [requestId, 5], + }, + }, + ); + }, + true, + ); + + await asyncForEach( + [5, 6, 7], + async (requestId) => { + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('1'), + ); + }, + true, + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 8, + parseUnits('1'), + ); + }); + }); + + describe('isAvgRate=true', async () => { + it('should fail: call from address without vault admin role', async () => { + const { + depositVault, + regularAccounts, + mTokenToUsdDataFeed, + mTBILL, + } = await loadDvFixture(); + await approveRequestTest( + { + depositVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + }, + 1, + parseUnits('1'), + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 1, + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('when instant part is 0', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + 0, + parseUnits('5'), + ); + }); + + it('if new rate greater then variabilityTolerance', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + requestId, + parseUnits('6'), + ); + }); + + it('if new rate lower then variabilityTolerance', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + requestId, + parseUnits('4'), + ); + }); + + it('should fail: request already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + requestId, + parseUnits('5.000001'), + ); + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + requestId, + parseUnits('5.000001'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('approve request should decrease pending supply', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 1000, + ); + await setMaxSupplyCapTest({ depositVault, owner }, 100); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 0, + parseUnits('1'), + ); + }); + + it('should fail: when after approval supply exceeds max cap', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 1000, + ); + await setMaxSupplyCapTest({ depositVault, owner }, 100); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 90, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 10, + ); + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 0, + parseUnits('0.99'), + { + revertCustomError: { + customErrorName: 'SupplyCapExceeded', + }, + }, + ); + }); + + it('should fail: when 0 supply cap is left', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + customRecipient, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 1000, + ); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMaxSupplyCapTest({ depositVault, owner }, 100); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 99, + { + from: regularAccounts[0], + }, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + }, + ); + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 0, + parseUnits('0.99'), + { + revertCustomError: { + customErrorName: 'SupplyCapExceeded', + }, + }, + ); + }); + + it('should fail: when 10 supply cap is left and try to mint more', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + customRecipient, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, regularAccounts[0], 101); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 101, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 1000, + ); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMaxSupplyCapTest({ depositVault, owner }, 100); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 90, + { + from: regularAccounts[0], + }, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 10, + { + from: regularAccounts[0], + }, + ); + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 0, + parseUnits('0.99'), + { + revertCustomError: { + customErrorName: 'SupplyCapExceeded', + }, + }, + ); + }); + + it('when 10 supply cap is left and try to mint 10', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + customRecipient, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setMaxSupplyCapTest({ depositVault, owner }, 100); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 90, + { + from: regularAccounts[0], + }, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 10, + { + from: regularAccounts[0], + }, + ); + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 0, + parseUnits('1'), + ); + }); + + it('approve request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + requestId, + parseUnits('5.000000001'), + ); + }); + + it('should succeed when other approve entrypoints are paused', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await pauseOtherDepositApproveFns( + depositVault, + encodeFnSelector('approveRequest(uint256,uint256,bool)'), + ); + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + requestId, + parseUnits('5'), + ); + }); + + it('should approve request in non sequential order when sequentialRequestProcessing is disabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 1000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 1, + parseUnits('1'), + ); + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 0, + parseUnits('1'), + ); + }); + + it('should fail: approve request in non sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 1000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + }, + true, + ); + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 0, + parseUnits('1'), + ); + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 2, + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'InvalidRequestSequence', + args: [2, 1], + }, + }, + ); + }); + + it('should approve request id 0 and then id 1 when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 1000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 0, + parseUnits('1'), + ); + + await approveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 1, + parseUnits('1'), + ); + }); + }); + }); + + describe('safeBulkApproveRequestAtSavedRate()', async () => { + it('should fail: call from address without vault admin role', async () => { + const { depositVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = + await loadDvFixture(); + await safeBulkApproveRequestTest( + { + depositVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + }, + [{ id: 1 }], + 'request-rate', + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + [{ id: 1 }], + 'request-rate', + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should skip requests above maxApproveRequestId without reverting', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await setMaxApproveRequestIdTest({ vault: depositVault, owner }, 0); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + 'request-rate', + ); + }); + + it('should fail: request already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + 'request-rate', + ); + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + 'request-rate', + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when one of them already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('5.000001'), + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + 'request-rate', + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when couple of them have equal id', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('5.000001'), + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 1 }], + 'request-rate', + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('approve 2 requests it should decrese the upcoming supply value fully', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountTest({ vault: depositVault, owner }, 0); + await setMaxSupplyCapTest({ depositVault, owner }, 100); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1 }], + 'request-rate', + ); + }); + + it('approve 1 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + 'request-rate', + ); + }); + + it('approve 2 requests from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1 }], + 'request-rate', + ); + }); + + it('approve 10 requests from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 1000); + await approveBase18(owner, stableCoins.dai, depositVault, 1000); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await asyncForEach( + Array.from({ length: 10 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + Array.from({ length: 10 }, (_, i) => ({ id: i })), + 'request-rate', + ); + }); + + it('should succeed when other approve entrypoints are paused', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await pauseOtherDepositApproveFns( + depositVault, + encodeFnSelector('safeBulkApproveRequestAtSavedRate(uint256[])'), + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + 'request-rate', + ); + }); + + it('should approve requests in non sequential order when sequentialRequestProcessing is disabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + 'request-rate', + ); + }); + + it('should skip out-of-order bulk approvals without reverting when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [ + { id: 1, expectedToExecute: false }, + { id: 0, expectedToExecute: true }, + ], + 'request-rate', + ); + }); + + it('should approve requests in sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1 }, { id: 2 }], + 'request-rate', + ); + }); + + it('should not approve requests after max supply cap when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountTest({ vault: depositVault, owner }, 0); + await setMaxSupplyCapTest({ depositVault, owner }, 100); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 40, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + parseUnits('0.899'), + ); + }); + }); + + describe('safeBulkApproveRequest() (custom price overload)', async () => { + it('should fail: call from address without vault admin role', async () => { + const { depositVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = + await loadDvFixture(); + await safeBulkApproveRequestTest( + { + depositVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + }, + [{ id: 1 }], + parseUnits('1'), + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + [{ id: 1 }], + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should skip requests above maxApproveRequestId without reverting', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await setMaxApproveRequestIdTest({ vault: depositVault, owner }, 0); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + parseUnits('5'), + ); + }); + + it('should fail: if new rate greater then variabilityTolerance', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + [{ id: requestId }], + parseUnits('6'), + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: if new rate lower then variabilityTolerance', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + [{ id: requestId }], + parseUnits('4'), + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: request already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + parseUnits('5.000001'), + ); + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + parseUnits('5.000001'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when one of them already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('5.000001'), + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + parseUnits('5.000001'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when couple of them have equal id', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('5.000001'), + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 1 }], + parseUnits('5.000001'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('approve 2 requests when second one exceeds supply cap', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountTest({ vault: depositVault, owner }, 0); + await setMaxSupplyCapTest({ depositVault, owner }, 100); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 40, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + parseUnits('0.899'), + ); + }); + + it('approve 1 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + parseUnits('5.000000001'), + ); + }); + + it('approve 2 requests from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1 }], + parseUnits('5.000000001'), + ); + }); + + it('approve 10 requests from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 1000); + await approveBase18(owner, stableCoins.dai, depositVault, 1000); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await asyncForEach( + Array.from({ length: 10 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + Array.from({ length: 10 }, (_, i) => ({ id: i })), + parseUnits('5.000000001'), + ); + }); + + it('should succeed when other approve entrypoints are paused', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await pauseOtherDepositApproveFns( + depositVault, + encodeFnSelector('safeBulkApproveRequest(uint256[],uint256)'), + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + parseUnits('5.000000001'), + ); + }); + + it('should approve requests in non sequential order when sequentialRequestProcessing is disabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + parseUnits('1'), + ); + }); + + it('should skip out-of-order bulk approvals without reverting when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [ + { id: 1, expectedToExecute: false }, + { id: 0, expectedToExecute: true }, + ], + parseUnits('1'), + ); + }); + + it('should approve requests in sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1 }, { id: 2 }], + parseUnits('1'), + ); + }); + + it('should not approve requests after max supply cap when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountTest({ vault: depositVault, owner }, 0); + await setMaxSupplyCapTest({ depositVault, owner }, 100); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 40, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + parseUnits('0.899'), + ); + }); + }); + + describe('safeBulkApproveRequestAvgRate() (custom price overload)', async () => { + it('should fail: call from address without vault admin role', async () => { + const { depositVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = + await loadDvFixture(); + await safeBulkApproveRequestTest( + { + depositVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }], + parseUnits('1'), + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }], + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should skip requests above maxApproveRequestId without reverting', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await setMaxApproveRequestIdTest({ vault: depositVault, owner }, 0); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + parseUnits('5'), + ); + }); + + it('should fail: if new rate greater then variabilityTolerance', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + parseUnits('6'), + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: if new rate lower then variabilityTolerance', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 95_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + parseUnits('4'), + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: request already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + parseUnits('5.000001'), + ); + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + parseUnits('5.000001'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when one of them already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('5.000001'), + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + parseUnits('5.000001'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when couple of them have equal id', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('5.000001'), + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 1 }], + parseUnits('5.000001'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('process multiple requests, when one of them does not have instant part', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + parseUnits('5.00001'), + ); + }); + + it('approve 2 requests when second one exceeds supply cap', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountTest({ vault: depositVault, owner }, 0); + await setMaxSupplyCapTest({ depositVault, owner }, 100); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 40, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + parseUnits('0.899'), + ); + }); + + it('approve 1 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + parseUnits('5.000000001'), + ); + }); + + it('approve 2 requests from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }, { id: 1 }], + parseUnits('5.000000001'), + ); + }); + + it('approve 10 requests from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 1000); + await approveBase18(owner, stableCoins.dai, depositVault, 1000); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await asyncForEach( + Array.from({ length: 10 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + Array.from({ length: 10 }, (_, i) => ({ id: i })), + parseUnits('5.000000001'), + ); + }); + + it('should succeed when other approve entrypoints are paused', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await pauseOtherDepositApproveFns( + depositVault, + encodeFnSelector( + 'safeBulkApproveRequestAvgRate(uint256[],uint256)', + ), + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + parseUnits('5.000000001'), + ); + }); + + it('should approve requests in non sequential order when sequentialRequestProcessing is disabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + parseUnits('5'), + ); + }); + + it('should skip out-of-order bulk approvals without reverting when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [ + { id: 1, expectedToExecute: false }, + { id: 0, expectedToExecute: true }, + ], + parseUnits('5'), + ); + }); + + it('should approve requests in sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 300); + await approveBase18(owner, stableCoins.dai, depositVault, 300); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }, { id: 1 }, { id: 2 }], + parseUnits('5'), + ); + }); + + it('should not approve requests after max supply cap when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountTest({ vault: depositVault, owner }, 0); + await setMaxSupplyCapTest({ depositVault, owner }, 100); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 40, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + parseUnits('0.899'), + ); + }); + }); + + describe('safeBulkApproveRequest() (current price overload)', async () => { + it('should fail: call from address without vault admin role', async () => { + const { depositVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = + await loadDvFixture(); + await safeBulkApproveRequestTest( + { + depositVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + }, + [{ id: 1 }], + undefined, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + [{ id: 1 }], + undefined, + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should skip requests above maxApproveRequestId without reverting', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await setMaxApproveRequestIdTest({ vault: depositVault, owner }, 0); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + undefined, + ); + }); + + it('should fail: if new rate greater then variabilityTolerance', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 10); + + const requestId = 0; + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + [{ id: requestId }], + undefined, + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: if new rate lower then variabilityTolerance', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 3); + + const requestId = 0; + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + [{ id: requestId }], + undefined, + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: request already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + undefined, + ); + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + undefined, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when one of them already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }], + undefined, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + undefined, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when couple of the have equal id', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }], + undefined, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 1 }], + undefined, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('approve 1 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + undefined, + ); + }); + + it('approve 1 request from vaut admin account when growth is applied', async () => { + const { + owner, + mockedAggregator, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + customFeedGrowth, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, 5); + await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + [{ id: requestId }], + undefined, + ); + }); + + it('approve 2 requests from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1 }], + undefined, + ); + }); + + it('approve 10 requests from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 1000); + await approveBase18(owner, stableCoins.dai, depositVault, 1000); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await asyncForEach( + Array.from({ length: 10 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + Array.from({ length: 10 }, (_, i) => ({ id: i })), + undefined, + ); + }); + + it('approve 10 requests from vaut admin account when different users are recievers', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 1000); + await approveBase18(owner, stableCoins.dai, depositVault, 1000); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await asyncForEach( + Array.from({ length: 10 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient: regularAccounts[i], + }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + Array.from({ length: 10 }, (_, i) => ({ id: i })), + undefined, + ); + }); + + it('approve 2 requests from vaut admin account when each request has different token', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await mintToken(stableCoins.usdc, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await approveBase18(owner, stableCoins.usdc, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.usdc, + 100, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1 }], + undefined, + ); + }); + + it('should succeed when other approve entrypoints are paused', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await pauseOtherDepositApproveFns( + depositVault, + encodeFnSelector('safeBulkApproveRequest(uint256[])'), + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + undefined, + ); + }); + + it('should approve requests in non sequential order when sequentialRequestProcessing is disabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + undefined, + ); + }); + + it('should skip out-of-order bulk approvals without reverting when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [ + { id: 1, expectedToExecute: false }, + { id: 0, expectedToExecute: true }, + ], + undefined, + ); + }); + + it('should approve requests in sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1 }, { id: 2 }], + undefined, + ); + }); + + it('should not approve requests after max supply cap when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountTest({ vault: depositVault, owner }, 0); + await setMaxSupplyCapTest({ depositVault, owner }, 100); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 40, + ); + + await safeBulkApproveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + parseUnits('0.899'), + ); + }); + }); + + describe('safeBulkApproveRequestAvgRate() (current price overload)', async () => { + it('should fail: call from address without vault admin role', async () => { + const { depositVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = + await loadDvFixture(); + await safeBulkApproveRequestTest( + { + depositVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }], + undefined, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }], + undefined, + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should skip requests above maxApproveRequestId without reverting', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await setMaxApproveRequestIdTest({ vault: depositVault, owner }, 0); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + undefined, + ); + }); + + it('should fail: if new rate greater then variabilityTolerance', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 1); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 10); + + const requestId = 0; + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: if new rate lower then variabilityTolerance', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 3); + + const requestId = 0; + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: request already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + ); + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when one of them already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }], + undefined, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + undefined, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when couple of the have equal id', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }], + undefined, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 1 }], + undefined, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('when one of the requests instant part is 0', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + undefined, + ); + }); + + it('approve 1 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + ); + }); + + it('approve 1 request from vaut admin account when growth is applied', async () => { + const { + owner, + mockedAggregator, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + customFeedGrowth, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, 5); + await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + ); + }); + + it('approve 2 requests from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }, { id: 1 }], + undefined, + ); + }); + + it('approve 10 requests from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 1000); + await approveBase18(owner, stableCoins.dai, depositVault, 1000); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await asyncForEach( + Array.from({ length: 10 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + Array.from({ length: 10 }, (_, i) => ({ id: i })), + undefined, + ); + }); + + it('approve 10 requests from vaut admin account when different users are recievers', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 1000); + await approveBase18(owner, stableCoins.dai, depositVault, 1000); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await asyncForEach( + Array.from({ length: 10 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient: regularAccounts[i], + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + Array.from({ length: 10 }, (_, i) => ({ id: i })), + undefined, + ); + }); + + it('approve 2 requests from vaut admin account when each request has different token', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await mintToken(stableCoins.usdc, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await approveBase18(owner, stableCoins.usdc, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.usdc, + 100, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }, { id: 1 }], + undefined, + ); + }); + + it('should succeed when other approve entrypoints are paused', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await pauseOtherDepositApproveFns( + depositVault, + encodeFnSelector('safeBulkApproveRequestAvgRate(uint256[])'), + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + ); + }); + + it('should approve requests in non sequential order when sequentialRequestProcessing is disabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + undefined, + ); + }); + + it('should skip out-of-order bulk approvals without reverting when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [ + { id: 1, expectedToExecute: false }, + { id: 0, expectedToExecute: true }, + ], + undefined, + ); + }); + + it('should approve requests in sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 300); + await approveBase18(owner, stableCoins.dai, depositVault, 300); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setVariabilityToleranceTest( + { vault: depositVault, owner }, + 2000, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }, { id: 1 }, { id: 2 }], + undefined, + ); + }); + }); + + describe('rejectRequest()', async () => { + it('should fail: call from address without vault admin role', async () => { + const { depositVault, regularAccounts, mTokenToUsdDataFeed, mTBILL } = + await loadDvFixture(); + await rejectRequestTest( + { + depositVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + }, + 1, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await rejectRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 1, + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should fail: request is already rejected', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await rejectRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + ); + + await rejectRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('reject request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await rejectRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + ); + }); + + it('rejecting request should decrease pending supply', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setMaxSupplyCapTest({ depositVault, owner }, 100); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setInstantFeeTest({ vault: depositVault, owner }, 0); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + + await rejectRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + ); + }); + + it('should reject request id 0 and then id 1 when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 200); + await approveBase18(owner, stableCoins.dai, depositVault, 200); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await rejectRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + ); + + await rejectRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 1, + ); + }); + + it('should fail: reject request id in non sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await setSequentialRequestProcessingTest( + { vault: depositVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, owner, 300); + await approveBase18(owner, stableCoins.dai, depositVault, 300); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async (i) => { + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await rejectRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + 2, + { + revertCustomError: { + customErrorName: 'InvalidRequestSequence', + args: [2, 0], + }, + }, + ); + }); + }); + + describe('depositInstant() complex', () => { + it('should fail: when is paused', async () => { + const { + depositVault, + owner, + mTBILL, + stableCoins, + regularAccounts, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await pauseVault({ pauseManager, owner }, depositVault); + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('is on pause, but admin can use everything', async () => { + const { + depositVault, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await pauseVault({ pauseManager, owner }, depositVault); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('call for amount == minAmountToDepositTest', async () => { + const { + depositVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await mintToken(stableCoins.dai, owner, 102_000); + await approveBase18(owner, stableCoins.dai, depositVault, 102_000); + + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountToDepositTest({ depositVault, owner }, 100_000); + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + parseUnits('150000'), + ); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 102_000, + ); + }); + + it('call for amount == minAmountToDepositTest+1, then deposit with amount 100', async () => { + const { + depositVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await mintToken(stableCoins.dai, owner, 103_101); + await approveBase18(owner, stableCoins.dai, depositVault, 103_101); + + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountToDepositTest({ depositVault, owner }, 100_000); + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + parseUnits('150000'), + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 103_001, + ); + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('deposit 100 DAI, when price is 5$, 25 USDC when price is 5.1$, 14 USDT when price is 5.4$', async () => { + const { + owner, + mockedAggregator, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await mintToken(stableCoins.usdc, owner, 125); + await mintToken(stableCoins.usdt, owner, 114); + + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await approveBase18(owner, stableCoins.usdc, depositVault, 125); + await approveBase18(owner, stableCoins.usdt, depositVault, 114); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.usdt, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.04); + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await setRoundData({ mockedAggregator }, 1); + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.usdc, + 125, + ); + + await setRoundData({ mockedAggregator }, 1.01); + await depositInstantTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.usdt, + 114, + ); + }); + }); + + describe('depositRequest() complex', () => { + it('should fail: when is paused', async () => { + const { + depositVault, + owner, + mTBILL, + stableCoins, + regularAccounts, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await pauseVault({ pauseManager, owner }, depositVault); + await mintToken(stableCoins.dai, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 100, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('is on pause, but admin can use everything', async () => { + const { + depositVault, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await pauseVault({ pauseManager, owner }, depositVault); + + await mintToken(stableCoins.dai, owner, 100); + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('call for amount == minAmountToDepositTest', async () => { + const { + depositVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await mintToken(stableCoins.dai, owner, 105_000); + await approveBase18(owner, stableCoins.dai, depositVault, 105_000); + + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountToDepositTest({ depositVault, owner }, 100_000); + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + 150_000, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 102_000, + ); + const requestId = 0; + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('5'), + ); + }); + + it('call for amount == minAmountToDepositTest+1, then deposit with amount 1', async () => { + const { + depositVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await mintToken(stableCoins.dai, owner, 105_101); + await approveBase18(owner, stableCoins.dai, depositVault, 105_101); + + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountToDepositTest({ depositVault, owner }, 100_000); + await setInstantLimitConfigTest( + { vault: depositVault, owner }, + 150_000, + ); + + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 102_001, + ); + let requestId = 0; + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('5'), + ); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + requestId = 1; + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('5'), + ); + }); + + it('deposit 100 DAI, when price is 5$, 25 USDC when price is 5.1$, 14 USDT when price is 5.4$', async () => { + const { + owner, + mockedAggregator, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadDvFixture(); + + await mintToken(stableCoins.dai, owner, 100); + await mintToken(stableCoins.usdc, owner, 125); + await mintToken(stableCoins.usdt, owner, 114); + + await approveBase18(owner, stableCoins.dai, depositVault, 100); + await approveBase18(owner, stableCoins.usdc, depositVault, 125); + await approveBase18(owner, stableCoins.usdt, depositVault, 114); + + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.usdt, + dataFeed.address, + 0, + true, + ); + await setMinAmountTest({ vault: depositVault, owner }, 10); + + await setRoundData({ mockedAggregator }, 1.04); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + let requestId = 0; + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('5'), + ); + + await setRoundData({ mockedAggregator }, 1); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.usdc, + 125, + ); + requestId = 1; + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('5'), + ); + + await setRoundData({ mockedAggregator }, 1.01); + await depositRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.usdt, + 114, + ); + requestId = 2; + + await approveRequestTest( + { depositVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('5'), + ); + }); + + it('should fail: when 10 supply cap is left and try to mint request 100', async () => { + const { + owner, + depositVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + customRecipient, + mockedAggregator, + mockedAggregatorMToken, + } = await loadDvFixture(); + await mintToken(stableCoins.dai, regularAccounts[0], 190); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + depositVault, + 190, + ); + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setMaxSupplyCapTest({ depositVault, owner }, 100); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountTest({ vault: depositVault, owner }, 0); + + await depositInstantTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 90, + { + from: regularAccounts[0], + }, + ); + + await depositRequestTest( + { + depositVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'SupplyCapExceeded', + }, + }, + ); + }); + }); + + describe('_convertUsdToToken', () => { + it('when amountUsd == 0', async () => { + const { depositVault, owner, stableCoins, dataFeed } = + await loadDvFixture(); + + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + expect( + ( + await depositVault.convertTokenToUsdTest( + stableCoins.dai.address, + 0, + ) + ).amountInUsd, + ).eq(0); + }); + + it('should fail: when unknown payment token', async () => { + const { depositVault } = await loadDvFixture(); + + await expect( + depositVault.convertTokenToUsdTest(constants.AddressZero, 0), + ).to.be.revertedWithoutReason(); + }); + + it('should fail: when tokenRate == 0', async () => { + const { depositVault, owner, stableCoins, dataFeed } = + await loadDvFixture(); + + await depositVault.setOverrideGetTokenRate(true); + await depositVault.setGetTokenRateValue(0); + + await addPaymentTokenTest( + { vault: depositVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await expect( + depositVault.convertTokenToUsdTest(stableCoins.dai.address, 1), + ).to.be.revertedWithCustomError(depositVault, 'InvalidTokenRate'); + }); + }); + + describe('_convertUsdToMToken', () => { + it('when amountUsd == 0', async () => { + const { depositVault } = await loadDvFixture(); + + expect( + (await depositVault.convertUsdToMTokenTest(0)).amountMToken, + ).eq(0); + }); + + it('should fail: when rate == 0', async () => { + const { depositVault } = await loadDvFixture(); + + await depositVault.setOverrideGetTokenRate(true); + await depositVault.setGetTokenRateValue(0); + + await expect( + depositVault.convertUsdToMTokenTest(1), + ).to.be.revertedWithCustomError(depositVault, 'InvalidTokenRate'); + }); + }); + + describe('_calculateHoldbackPartRateFromAvg', () => { + it('returns 0 when depositedUsdAmount is 0', async () => { + const { depositVault } = await loadDvFixture(); + const depositedUsdAmount = 0; + const amountTokenInstant = parseUnits('50'); + const avgMTokenRate = parseUnits('1'); + const tokenOutRate = parseUnits('1'); + const expected = expectedDepositHoldbackPartRateFromAvg( + 0n, + BigInt(amountTokenInstant.toString()), + BigInt(tokenOutRate.toString()), + BigInt(avgMTokenRate.toString()), + ); + expect(expected).eq(0n); + expect( + await depositVault.calculateHoldbackPartRateFromAvgTest( + depositedUsdAmount, + amountTokenInstant, + tokenOutRate, + avgMTokenRate, + ), + ).eq(expected.toString()); + }); + + it('returns 0 when avg rate implies lower target value than instant leg', async () => { + const { depositVault } = await loadDvFixture(); + const depositedUsdAmount = parseUnits('10'); + const amountTokenInstant = parseUnits('100'); + const avgMTokenRate = parseUnits('1'); + const tokenOutRate = parseUnits('0.5'); + const expected = expectedDepositHoldbackPartRateFromAvg( + BigInt(depositedUsdAmount.toString()), + BigInt(amountTokenInstant.toString()), + BigInt(tokenOutRate.toString()), + BigInt(avgMTokenRate.toString()), + ); + expect(expected).eq(0n); + expect( + await depositVault.calculateHoldbackPartRateFromAvgTest( + depositedUsdAmount, + amountTokenInstant, + tokenOutRate, + avgMTokenRate, + ), + ).eq(expected.toString()); + }); + + it('returns 0 when avgMTokenRate is 0', async () => { + const { depositVault } = await loadDvFixture(); + const depositedUsdAmount = parseUnits('100'); + const amountTokenInstant = parseUnits('0'); + const one = parseUnits('1'); + const expected = expectedDepositHoldbackPartRateFromAvg( + BigInt(depositedUsdAmount.toString()), + BigInt(amountTokenInstant.toString()), + BigInt(one.toString()), + 0n, + ); + expect( + await depositVault.calculateHoldbackPartRateFromAvgTest( + depositedUsdAmount, + amountTokenInstant, + one, + 0, + ), + ).eq(expected.toString()); + }); + + it('returns 0 when depositedInstantUsdAmount is 0', async () => { + const { depositVault } = await loadDvFixture(); + const depositedUsdAmount = parseUnits('100'); + const tokenOutRate = parseUnits('1'); + const avgMTokenRate = parseUnits('1.25'); + const expected = expectedDepositHoldbackPartRateFromAvg( + BigInt(depositedUsdAmount.toString()), + 0n, + BigInt(tokenOutRate.toString()), + BigInt(avgMTokenRate.toString()), + ); + expect(expected).eq(0n); + expect( + await depositVault.calculateHoldbackPartRateFromAvgTest( + depositedUsdAmount, + 0, + tokenOutRate, + avgMTokenRate, + ), + ).eq(expected.toString()); + }); + + it('applies integer rounding on the final rate', async () => { + const { depositVault } = await loadDvFixture(); + const depositedUsdAmount = 3n; + const amountTokenInstant = 1n; + const tokenOutRate = 3n; + const avgMTokenRate = 2n; + const expected = expectedDepositHoldbackPartRateFromAvg( + depositedUsdAmount, + amountTokenInstant, + tokenOutRate, + avgMTokenRate, + ); + expect( + await depositVault.calculateHoldbackPartRateFromAvgTest( + depositedUsdAmount, + amountTokenInstant, + tokenOutRate, + avgMTokenRate, + ), + ).eq(expected.toString()); + }); + + it('succeeds with depositedUsdAmount == 0 when branch returns 0 before division', async () => { + const { depositVault } = await loadDvFixture(); + const amountTokenInstant = parseUnits('100'); + const tokenOutRate = parseUnits('1'); + const avgMTokenRate = parseUnits('1'); + const expected = expectedDepositHoldbackPartRateFromAvg( + 0n, + BigInt(amountTokenInstant.toString()), + BigInt(tokenOutRate.toString()), + BigInt(avgMTokenRate.toString()), + ); + expect(expected).eq(0n); + expect( + await depositVault.calculateHoldbackPartRateFromAvgTest( + 0, + amountTokenInstant, + tokenOutRate, + avgMTokenRate, + ), + ).eq('0'); + }); + + it('returns 0 when depositedUsdAmount == 0 and holdback part is positive', async () => { + const { depositVault } = await loadDvFixture(); + const amountTokenInstant = parseUnits('100'); + const avgMTokenRate = parseUnits('1'); + const tokenOutRate = parseUnits('2'); + const expected = expectedDepositHoldbackPartRateFromAvg( + 0n, + BigInt(amountTokenInstant.toString()), + BigInt(tokenOutRate.toString()), + BigInt(avgMTokenRate.toString()), + ); + expect(expected).eq(0n); + expect( + await depositVault.calculateHoldbackPartRateFromAvgTest( + 0, + amountTokenInstant, + tokenOutRate, + avgMTokenRate, + ), + ).eq(expected.toString()); + }); + + it('matches reference for mixed instant and holdback with realistic WAD rates', async () => { + const { depositVault } = await loadDvFixture(); + const depositedUsdAmount = parseUnits('70'); + const amountTokenInstant = parseUnits('30'); + const avgMTokenRate = parseUnits('1'); + const tokenOutRate = parseUnits('1'); + const expected = expectedDepositHoldbackPartRateFromAvg( + BigInt(depositedUsdAmount.toString()), + BigInt(amountTokenInstant.toString()), + BigInt(tokenOutRate.toString()), + BigInt(avgMTokenRate.toString()), + ); + expect( + await depositVault.calculateHoldbackPartRateFromAvgTest( + depositedUsdAmount, + amountTokenInstant, + tokenOutRate, + avgMTokenRate, + ), + ).eq(expected.toString()); + }); + + it('handles large values without overflow when inputs are bounded', async () => { + const { depositVault } = await loadDvFixture(); + const depositedUsdAmount = 10n ** 30n * 6n; + const amountTokenInstant = 10n ** 30n * 4n; + const avgMTokenRate = 10n ** 18n * 2n; + const tokenOutRate = 10n ** 18n * 5n; + const expected = expectedDepositHoldbackPartRateFromAvg( + depositedUsdAmount, + amountTokenInstant, + tokenOutRate, + avgMTokenRate, + ); + expect( + await depositVault.calculateHoldbackPartRateFromAvgTest( + depositedUsdAmount, + amountTokenInstant, + tokenOutRate, + avgMTokenRate, + ), + ).eq(expected.toString()); + }); + }); + }); + + otherTests(dvFixture); + }); +}; diff --git a/test/unit/suits/manageable-vault.suits.ts b/test/unit/suits/manageable-vault.suits.ts new file mode 100644 index 00000000..f02f1e28 --- /dev/null +++ b/test/unit/suits/manageable-vault.suits.ts @@ -0,0 +1,2920 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { increase } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time'; +import { days } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time/duration'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { constants } from 'ethers'; +import { parseUnits } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; + +import { encodeFnSelector } from '../../../helpers/utils'; +import { + ManageableVaultTester, + ManageableVaultTester__factory, +} from '../../../typechain-types'; +import { + acErrors, + setPermissionRoleTester, + setRoleTimelocksTester, + setupGrantOperatorRole, + setupPermissionRole, +} from '../../common/ac.helpers'; +import { + mintToken, + pauseVaultFn, + approveBase18, + InitializeParamCase, +} from '../../common/common.helpers'; +import { + DefaultFixture, + getInitializerParamsMv, + InitializerParamsMv, +} from '../../common/fixtures'; +import { greenListEnable } from '../../common/greenlist.helpers'; +import { + addPaymentTokenTest, + removePaymentTokenTest, + setVariabilityToleranceTest, + withdrawTest, + changeTokenAllowanceTest, + changeTokenFeeTest, + setWaivedFeeAccountTest, + setInstantFeeTest, + setMinAmountTest, + setMinMaxInstantFeeTest, + setSequentialRequestProcessingTest, + setTokensReceiverTest, + setInstantLimitConfigTest, + removeInstantLimitConfigTest, + setMaxInstantShareTest, + setMaxApproveRequestIdTest, +} from '../../common/manageable-vault.helpers'; +import { + bulkScheduleTimelockOperationTester, + executeTimelockOperationTester, +} from '../../common/timelock-manager.helpers'; + +const setTokensReceiverSelector = encodeFnSelector( + 'setTokensReceiver(address)', +); +const setInstantLimitConfigSelector = encodeFnSelector( + 'setInstantLimitConfig(uint256,uint256)', +); +const removeInstantLimitConfigSelector = encodeFnSelector( + 'removeInstantLimitConfig(uint256)', +); +const setMaxApproveRequestIdSelector = encodeFnSelector( + 'setMaxApproveRequestId(uint256)', +); +const setMaxInstantShareSelector = encodeFnSelector( + 'setMaxInstantShare(uint256)', +); +const setSequentialRequestProcessingSelector = encodeFnSelector( + 'setSequentialRequestProcessing(bool)', +); +let pauseManager: DefaultFixture['pauseManager']; +let owner: DefaultFixture['owner']; + +export const baseInitParamsMv = ( + fixture: DefaultFixture, +): InitializerParamsMv => ({ + accessControl: fixture.accessControl, + mockedSanctionsList: fixture.mockedSanctionsList, + mTBILL: fixture.mTBILL, + mTokenToUsdDataFeed: fixture.mTokenToUsdDataFeed, + tokensReceiver: fixture.tokensReceiver, +}); + +export const mvInitializeParamCases: InitializeParamCase[] = + [ + { + title: 'accessControl is zero address', + params: { accessControl: constants.AddressZero }, + revertCustomError: { + customErrorName: 'InvalidAddress', + args: [constants.AddressZero], + }, + }, + { + title: 'mTBILL is zero address', + params: { mTBILL: constants.AddressZero }, + revertCustomError: { + customErrorName: 'InvalidAddress', + args: [constants.AddressZero], + }, + }, + { + title: 'mTokenToUsdDataFeed is zero address', + params: { mTokenToUsdDataFeed: constants.AddressZero }, + revertCustomError: { + customErrorName: 'InvalidAddress', + args: [constants.AddressZero], + }, + }, + { + title: 'tokensReceiver is zero address', + params: { tokensReceiver: constants.AddressZero }, + revertCustomError: { + customErrorName: 'InvalidAddress', + args: [constants.AddressZero], + }, + }, + { + title: 'variationTolerance is zero', + params: { variationTolerance: 0 }, + revertCustomError: { customErrorName: 'InvalidFee', args: [0] }, + }, + { + title: 'variationTolerance is greater than 100%', + params: { variationTolerance: 10001 }, + revertCustomError: { customErrorName: 'InvalidFee', args: [10001] }, + }, + { + title: 'instantFee is greater than 100%', + params: { instantFee: 10001 }, + revertCustomError: { customErrorName: 'InvalidFee', args: [10001] }, + }, + { + title: 'maxInstantShare is greater than 100%', + params: { maxInstantShare: 10001 }, + revertCustomError: { customErrorName: 'InvalidFee', args: [10001] }, + }, + { + title: 'minInstantFee is greater than 100%', + params: { minInstantFee: 10001 }, + revertCustomError: { customErrorName: 'InvalidFee', args: [10001] }, + }, + { + title: 'maxInstantFee is greater than 100%', + params: { maxInstantFee: 10001 }, + revertCustomError: { customErrorName: 'InvalidFee', args: [10001] }, + }, + { + title: 'minInstantFee is greater than maxInstantFee', + params: { minInstantFee: 500, maxInstantFee: 100 }, + revertCustomError: { + customErrorName: 'InvalidMinMaxInstantFee', + args: [500, 100], + }, + }, + ]; + +export const manageableVaultSuits = ( + mvFixture: () => Promise, + mvConfig: { + createNew: (owner: SignerWithAddress) => Promise; + key: + | 'depositVault' + | 'depositVaultWithUSTB' + | 'depositVaultWithMToken' + | 'depositVaultWithAave' + | 'depositVaultWithMorpho' + | 'redemptionVault' + | 'redemptionVaultWithAave' + | 'redemptionVaultWithMToken' + | 'redemptionVaultWithUSTB' + | 'redemptionVaultWithMorpho'; + }, + deploymentAdditionalChecks: ( + fixtureRes: DefaultFixture, + ) => Promise = async () => {}, + otherTests: (fixture: () => Promise) => void = async () => {}, +) => { + const loadMvFixture = async () => { + const fixture = await loadFixture(mvFixture); + ({ pauseManager, owner } = fixture); + + const { createNew, key } = mvConfig; + return { + ...fixture, + originalVault: fixture.manageableVault, + manageableVault: ManageableVaultTester__factory.connect( + fixture[key].address, + fixture.owner, + ), + createNew: async () => { + const mv = await createNew(fixture.owner); + return ManageableVaultTester__factory.connect( + mv.address, + fixture.owner, + ); + }, + }; + }; + + describe('ManageableVault', function () { + it('deployment', async () => { + const fixture = await loadMvFixture(); + const { manageableVault, mTBILL, tokensReceiver, mTokenToUsdDataFeed } = + fixture; + + expect(await manageableVault.mToken()).eq(mTBILL.address); + + expect(await manageableVault.ONE_HUNDRED_PERCENT()).eq('10000'); + + expect(await manageableVault.tokensReceiver()).eq(tokensReceiver.address); + + expect(await manageableVault.minAmount()).eq(1000); + + expect(await manageableVault.instantFee()).eq('100'); + + expect(await manageableVault.mTokenDataFeed()).eq( + mTokenToUsdDataFeed.address, + ); + expect(await manageableVault.variationTolerance()).eq(1); + + expect(await manageableVault.minInstantFee()).eq(0); + expect(await manageableVault.maxInstantFee()).eq(10000); + expect((await manageableVault.getInstantLimitStatuses()).length).eq(0); + + await deploymentAdditionalChecks({ + ...fixture, + manageableVault: fixture.originalVault as ManageableVaultTester, + }); + }); + + describe('initialization', () => { + it('should fail: cal; initialize() when already initialized', async () => { + const { manageableVault } = await loadMvFixture(); + + await expect( + manageableVault.initializeExternal( + ...getInitializerParamsMv({ + accessControl: constants.AddressZero, + mockedSanctionsList: constants.AddressZero, + mTBILL: constants.AddressZero, + mTokenToUsdDataFeed: constants.AddressZero, + tokensReceiver: constants.AddressZero, + }), + ), + ).revertedWith('Initializable: contract is already initialized'); + }); + + it('should fail: call with initializing == false', async () => { + const { owner } = await loadMvFixture(); + + const vault = await new ManageableVaultTester__factory(owner).deploy(); + + await expect( + vault.initializeWithoutInitializer( + ...getInitializerParamsMv({ + accessControl: constants.AddressZero, + mockedSanctionsList: constants.AddressZero, + mTBILL: constants.AddressZero, + mTokenToUsdDataFeed: constants.AddressZero, + tokensReceiver: constants.AddressZero, + }), + ), + ).revertedWith('Initializable: contract is not initializing'); + }); + }); + + describe('common', () => { + describe('setInstantLimitConfig()', () => { + const instantLimitWindow = days(2); + + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + await setInstantLimitConfigTest( + { vault: manageableVault, owner }, + { window: instantLimitWindow, limit: parseUnits('1000') }, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault } = await loadMvFixture(); + await setInstantLimitConfigTest( + { vault: manageableVault, owner }, + { window: instantLimitWindow, limit: parseUnits('1000') }, + ); + }); + + it('should fail: when function is paused', async () => { + const { owner, manageableVault } = await loadMvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + setInstantLimitConfigSelector, + ); + + await setInstantLimitConfigTest( + { vault: manageableVault, owner }, + { window: instantLimitWindow, limit: parseUnits('1000') }, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setInstantLimitConfig(uint256,uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await setInstantLimitConfigTest( + { vault: manageableVault, owner }, + { window: instantLimitWindow, limit: parseUnits('1000') }, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + } = await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setInstantLimitConfig(uint256,uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await setInstantLimitConfigTest( + { vault: manageableVault, owner }, + { window: instantLimitWindow, limit: parseUnits('1000') }, + { from: regularAccounts[0] }, + ); + }); + + it('when updating existing window limit config', async () => { + const { manageableVault, owner } = await loadMvFixture(); + + await setInstantLimitConfigTest( + { vault: manageableVault, owner }, + { window: days(1), limit: parseUnits('500') }, + ); + await setInstantLimitConfigTest( + { vault: manageableVault, owner }, + { window: days(1), limit: parseUnits('1500') }, + ); + }); + }); + + describe('removeInstantLimitConfig()', () => { + const removeWindow = days(3); + + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + await removeInstantLimitConfigTest( + { vault: manageableVault, owner }, + removeWindow, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault } = await loadMvFixture(); + + await setInstantLimitConfigTest( + { vault: manageableVault, owner }, + { window: removeWindow, limit: parseUnits('1000') }, + ); + await removeInstantLimitConfigTest( + { vault: manageableVault, owner }, + removeWindow, + ); + }); + + it('should fail: when function is paused', async () => { + const { owner, manageableVault } = await loadMvFixture(); + + await setInstantLimitConfigTest( + { vault: manageableVault, owner }, + { window: removeWindow, limit: parseUnits('1000') }, + ); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + removeInstantLimitConfigSelector, + ); + + await removeInstantLimitConfigTest( + { vault: manageableVault, owner }, + removeWindow, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + await setInstantLimitConfigTest( + { vault: manageableVault, owner }, + { window: removeWindow, limit: parseUnits('1000') }, + ); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'removeInstantLimitConfig(uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await removeInstantLimitConfigTest( + { vault: manageableVault, owner }, + removeWindow, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + } = await loadMvFixture(); + + await setInstantLimitConfigTest( + { vault: manageableVault, owner }, + { window: removeWindow, limit: parseUnits('1000') }, + ); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'removeInstantLimitConfig(uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await removeInstantLimitConfigTest( + { vault: manageableVault, owner }, + removeWindow, + { from: regularAccounts[0] }, + ); + }); + + it('should fail: when window does not exist', async () => { + const { manageableVault, owner } = await loadMvFixture(); + await removeInstantLimitConfigTest( + { vault: manageableVault, owner }, + days(99), + { + revertCustomError: { + customErrorName: 'UnknownWindowLimit', + }, + }, + ); + }); + }); + + describe('setMaxApproveRequestId()', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + await setMaxApproveRequestIdTest( + { vault: manageableVault, owner }, + 250, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault } = await loadMvFixture(); + await setMaxApproveRequestIdTest( + { vault: manageableVault, owner }, + 250, + ); + }); + + it('should fail: when function is paused', async () => { + const { owner, manageableVault } = await loadMvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + setMaxApproveRequestIdSelector, + ); + + await setMaxApproveRequestIdTest( + { vault: manageableVault, owner }, + 250, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setMaxApproveRequestId(uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await setMaxApproveRequestIdTest( + { vault: manageableVault, owner }, + 250, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + } = await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setMaxApproveRequestId(uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await setMaxApproveRequestIdTest( + { vault: manageableVault, owner }, + 250, + { from: regularAccounts[0] }, + ); + }); + }); + + describe('setMaxInstantShare()', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + await setMaxInstantShareTest( + { vault: manageableVault, owner }, + 5000, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault } = await loadMvFixture(); + await setMaxInstantShareTest({ vault: manageableVault, owner }, 5000); + }); + + it('should fail: when function is paused', async () => { + const { owner, manageableVault } = await loadMvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + setMaxInstantShareSelector, + ); + + await setMaxInstantShareTest( + { vault: manageableVault, owner }, + 5000, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setMaxInstantShare(uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await setMaxInstantShareTest( + { vault: manageableVault, owner }, + 5000, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + } = await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setMaxInstantShare(uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await setMaxInstantShareTest( + { vault: manageableVault, owner }, + 5000, + { from: regularAccounts[0] }, + ); + }); + + it('should fail: if new value greater than 100%', async () => { + const { manageableVault, owner } = await loadMvFixture(); + await setMaxInstantShareTest( + { vault: manageableVault, owner }, + 10001, + { + revertCustomError: { + customErrorName: 'InvalidFee', + }, + }, + ); + }); + }); + + describe('setSequentialRequestProcessing()', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + await setSequentialRequestProcessingTest( + { vault: manageableVault, owner }, + true, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault } = await loadMvFixture(); + await setSequentialRequestProcessingTest( + { vault: manageableVault, owner }, + true, + ); + }); + + it('should fail: when function is paused', async () => { + const { owner, manageableVault } = await loadMvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + setSequentialRequestProcessingSelector, + ); + + await setSequentialRequestProcessingTest( + { vault: manageableVault, owner }, + true, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setSequentialRequestProcessing(bool)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await setSequentialRequestProcessingTest( + { vault: manageableVault, owner }, + true, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + } = await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setSequentialRequestProcessing(bool)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await setSequentialRequestProcessingTest( + { vault: manageableVault, owner }, + true, + { from: regularAccounts[0] }, + ); + }); + + it('should fail: if value is already set', async () => { + const { manageableVault, owner } = await loadMvFixture(); + await setSequentialRequestProcessingTest( + { vault: manageableVault, owner }, + true, + ); + await setSequentialRequestProcessingTest( + { vault: manageableVault, owner }, + true, + { + revertCustomError: { + customErrorName: 'SameBoolValue', + args: [true], + }, + }, + ); + }); + }); + + describe('setMinAmount()', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + await setMinAmountTest({ vault: manageableVault, owner }, 1.1, { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault } = await loadMvFixture(); + await setMinAmountTest({ vault: manageableVault, owner }, 1.1); + }); + + it('should fail: when function is paused', async () => { + const { owner, manageableVault } = await loadMvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + encodeFnSelector('setMinAmount(uint256)'), + ); + + await setMinAmountTest({ vault: manageableVault, owner }, 1.1, { + revertCustomError: { + customErrorName: 'Paused', + }, + }); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setMinAmount(uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await setMinAmountTest({ vault: manageableVault, owner }, 200, { + from: regularAccounts[0], + }); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + } = await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setMinAmount(uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await setMinAmountTest({ vault: manageableVault, owner }, 200, { + from: regularAccounts[0], + }); + }); + }); + + describe('setTokensReceiver()', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault, regularAccounts, tokensReceiver } = + await loadMvFixture(); + + await setTokensReceiverTest( + { vault: manageableVault, owner }, + tokensReceiver.address, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: if receiver is zero address', async () => { + const { owner, manageableVault } = await loadMvFixture(); + + await setTokensReceiverTest( + { vault: manageableVault, owner }, + constants.AddressZero, + { + revertCustomError: { + customErrorName: 'InvalidAddress', + args: [constants.AddressZero], + }, + }, + ); + }); + + it('should fail: if receiver is vault address', async () => { + const { owner, manageableVault } = await loadMvFixture(); + + await setTokensReceiverTest( + { vault: manageableVault, owner }, + manageableVault.address, + { + revertCustomError: { + customErrorName: 'InvalidAddress', + args: [manageableVault.address], + }, + }, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + await setTokensReceiverTest( + { vault: manageableVault, owner }, + regularAccounts[1].address, + ); + }); + + it('should fail: when function is paused', async () => { + const { owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + setTokensReceiverSelector, + ); + + await setTokensReceiverTest( + { vault: manageableVault, owner }, + regularAccounts[1].address, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setTokensReceiver(address)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await setTokensReceiverTest( + { vault: manageableVault, owner }, + regularAccounts[1].address, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + } = await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setTokensReceiver(address)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await setTokensReceiverTest( + { vault: manageableVault, owner }, + regularAccounts[1].address, + { from: regularAccounts[0] }, + ); + }); + + it('when called through timelock with contract admin role', async () => { + const { + accessControl, + manageableVault, + owner, + regularAccounts, + timelock, + timelockManager, + } = await loadMvFixture(); + + const proposer = regularAccounts[0]; + const newReceiver = regularAccounts[1].address; + const vaultRole = await manageableVault.contractAdminRole(); + + await accessControl['grantRole(bytes32,address)']( + vaultRole, + proposer.address, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [vaultRole], + [3600], + ); + + const calldata = manageableVault.interface.encodeFunctionData( + 'setTokensReceiver', + [newReceiver], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [manageableVault.address], + [calldata], + {}, + { from: proposer }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + manageableVault.address, + calldata, + proposer.address, + { from: owner }, + ); + + expect(await manageableVault.tokensReceiver()).eq(newReceiver); + }); + + it('when called through timelock with function admin role', async () => { + const { + accessControl, + manageableVault, + owner, + regularAccounts, + timelock, + timelockManager, + } = await loadMvFixture(); + + const proposer = regularAccounts[0]; + const newReceiver = regularAccounts[1].address; + const vaultRole = await manageableVault.contractAdminRole(); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: vaultRole, + targetContract: manageableVault.address, + functionSelector: setTokensReceiverSelector, + grantOperator: owner, + }); + + await setupGrantOperatorRole({ + accessControl, + owner, + masterRole: vaultRole, + targetContract: timelockManager.address, + functionSelector: setTokensReceiverSelector, + grantOperator: owner, + }); + + await setPermissionRoleTester( + { accessControl, owner }, + vaultRole, + manageableVault.address, + setTokensReceiverSelector, + [{ account: proposer.address, enabled: true }], + ); + + await setPermissionRoleTester( + { accessControl, owner }, + vaultRole, + timelockManager.address, + setTokensReceiverSelector, + [{ account: proposer.address, enabled: true }], + ); + + expect(await accessControl.hasRole(vaultRole, proposer.address)).eq( + false, + ); + + const vaultPermissionKey = await accessControl.permissionRoleKey( + vaultRole, + manageableVault.address, + setTokensReceiverSelector, + ); + const timelockPermissionKey = await accessControl.permissionRoleKey( + vaultRole, + timelockManager.address, + setTokensReceiverSelector, + ); + + await setRoleTimelocksTester( + { timelockManager, timelock, owner, accessControl }, + [vaultPermissionKey, timelockPermissionKey], + [3600, 3600], + ); + + const calldata = manageableVault.interface.encodeFunctionData( + 'setTokensReceiver', + [newReceiver], + ); + + await bulkScheduleTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + [manageableVault.address], + [calldata], + {}, + { from: proposer }, + ); + + await increase(3600); + + await executeTimelockOperationTester( + { timelockManager, timelock, owner, accessControl }, + manageableVault.address, + calldata, + proposer.address, + { from: owner }, + ); + + expect(await manageableVault.tokensReceiver()).eq(newReceiver); + }); + }); + + describe('setGreenlistEnable()', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + await greenListEnable( + { greenlistable: manageableVault, owner }, + true, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault } = await loadMvFixture(); + + await greenListEnable( + { greenlistable: manageableVault, owner }, + true, + ); + }); + + it('should fail: when function is paused', async () => { + const { owner, manageableVault } = await loadMvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + encodeFnSelector('setGreenlistEnable(bool)'), + ); + + await greenListEnable( + { greenlistable: manageableVault, owner }, + true, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setGreenlistEnable(bool)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await greenListEnable( + { greenlistable: manageableVault, owner }, + true, + { + from: regularAccounts[0], + }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + } = await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setGreenlistEnable(bool)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await greenListEnable( + { greenlistable: manageableVault, owner }, + true, + { + from: regularAccounts[0], + }, + ); + }); + }); + + describe('addPaymentToken()', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, regularAccounts, owner } = + await loadMvFixture(); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + ethers.constants.AddressZero, + ethers.constants.AddressZero, + 0, + false, + constants.MaxUint256, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when token is already added', async () => { + const { manageableVault, stableCoins, owner, dataFeed } = + await loadMvFixture(); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + false, + ); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + false, + constants.MaxUint256, + { + revertCustomError: { + customErrorName: 'PaymentTokenAlreadyAdded', + }, + }, + ); + }); + + it('should fail: when token dataFeed address zero', async () => { + const { manageableVault, stableCoins, owner } = await loadMvFixture(); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + constants.AddressZero, + 0, + false, + constants.MaxUint256, + { + revertCustomError: { + customErrorName: 'InvalidAddress', + }, + }, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, stableCoins, owner, dataFeed } = + await loadMvFixture(); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + false, + ); + }); + + it('call when allowance is zero', async () => { + const { manageableVault, stableCoins, owner, dataFeed } = + await loadMvFixture(); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + false, + constants.Zero, + ); + }); + + it('call when allowance is not uint256 max', async () => { + const { manageableVault, stableCoins, owner, dataFeed } = + await loadMvFixture(); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + false, + parseUnits('100'), + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role and add 3 options on a row', async () => { + const { manageableVault, stableCoins, owner, dataFeed } = + await loadMvFixture(); + + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdt, + dataFeed.address, + 0, + true, + ); + }); + + it('should fail: when function is paused', async () => { + const { manageableVault, stableCoins, owner, dataFeed } = + await loadMvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + encodeFnSelector( + 'addPaymentToken(address,address,uint256,uint256,bool)', + ), + ); + + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + constants.MaxUint256, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + stableCoins, + dataFeed, + } = await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'addPaymentToken(address,address,uint256,uint256,bool)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + constants.MaxUint256, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + stableCoins, + dataFeed, + } = await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'addPaymentToken(address,address,uint256,uint256,bool)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdt, + dataFeed.address, + 0, + true, + constants.MaxUint256, + { from: regularAccounts[0] }, + ); + }); + }); + + describe('setWaivedFeeAccount()', () => { + describe('enabled=true', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, regularAccounts, owner } = + await loadMvFixture(); + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + ethers.constants.AddressZero, + true, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + from: regularAccounts[0], + }, + ); + }); + it('should fail: if account fee already waived', async () => { + const { manageableVault, owner } = await loadMvFixture(); + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + owner.address, + true, + ); + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + owner.address, + true, + { + revertCustomError: { + customErrorName: 'SameBoolValue', + }, + }, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, owner } = await loadMvFixture(); + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + owner.address, + true, + ); + }); + + it('should fail: when function is paused', async () => { + const { manageableVault, owner } = await loadMvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + encodeFnSelector('setWaivedFeeAccount(address,bool)'), + ); + + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + owner.address, + true, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setWaivedFeeAccount(address,bool)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole( + vaultRole, + regularAccounts[0].address, + ), + ).eq(false); + + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + regularAccounts[1].address, + true, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + } = await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setWaivedFeeAccount(address,bool)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + regularAccounts[2].address, + true, + { from: regularAccounts[0] }, + ); + }); + }); + + describe('enabled=false', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, regularAccounts, owner } = + await loadMvFixture(); + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + ethers.constants.AddressZero, + false, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + from: regularAccounts[0], + }, + ); + }); + it('should fail: if account not found in restriction', async () => { + const { manageableVault, owner } = await loadMvFixture(); + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + owner.address, + false, + { + revertCustomError: { + customErrorName: 'SameBoolValue', + }, + }, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, owner } = await loadMvFixture(); + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + owner.address, + true, + ); + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + owner.address, + false, + ); + }); + + it('should fail: when function is paused', async () => { + const { manageableVault, owner } = await loadMvFixture(); + + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + owner.address, + true, + ); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + encodeFnSelector('setWaivedFeeAccount(address,bool)'), + ); + + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + owner.address, + false, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + regularAccounts[1].address, + true, + ); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setWaivedFeeAccount(address,bool)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole( + vaultRole, + regularAccounts[0].address, + ), + ).eq(false); + + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + regularAccounts[1].address, + false, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + } = await loadMvFixture(); + + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + regularAccounts[2].address, + true, + ); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setWaivedFeeAccount(address,bool)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await setWaivedFeeAccountTest( + { vault: manageableVault, owner }, + regularAccounts[2].address, + false, + { from: regularAccounts[0] }, + ); + }); + }); + }); + + describe('setFee()', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, regularAccounts, owner } = + await loadMvFixture(); + await setInstantFeeTest( + { vault: manageableVault, owner }, + ethers.constants.Zero, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + from: regularAccounts[0], + }, + ); + }); + + it('should fail: if new value greater then 100%', async () => { + const { manageableVault, owner } = await loadMvFixture(); + await setInstantFeeTest({ vault: manageableVault, owner }, 10001, { + revertCustomError: { + customErrorName: 'InvalidFee', + }, + }); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, owner } = await loadMvFixture(); + await setInstantFeeTest({ vault: manageableVault, owner }, 100); + }); + + it('should fail: when function is paused', async () => { + const { manageableVault, owner } = await loadMvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + encodeFnSelector('setInstantFee(uint256)'), + ); + + await setInstantFeeTest({ vault: manageableVault, owner }, 100, { + revertCustomError: { + customErrorName: 'Paused', + }, + }); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setInstantFee(uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await setInstantFeeTest({ vault: manageableVault, owner }, 100, { + from: regularAccounts[0], + }); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + } = await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setInstantFee(uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await setInstantFeeTest({ vault: manageableVault, owner }, 100, { + from: regularAccounts[0], + }); + }); + }); + + describe('setMinMaxInstantFee()', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { owner, manageableVault, regularAccounts } = + await loadMvFixture(); + await setMinMaxInstantFeeTest( + { vault: manageableVault, owner }, + 0, + 1000, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + from: regularAccounts[0], + }, + ); + }); + + it('should fail: if min greater than max', async () => { + const { manageableVault, owner } = await loadMvFixture(); + await setMinMaxInstantFeeTest( + { vault: manageableVault, owner }, + 500, + 100, + { + revertCustomError: { + customErrorName: 'InvalidMinMaxInstantFee', + }, + }, + ); + }); + + it('should fail: if fee greater than 100%', async () => { + const { manageableVault, owner } = await loadMvFixture(); + await setMinMaxInstantFeeTest( + { vault: manageableVault, owner }, + 10001, + 10001, + { + revertCustomError: { + customErrorName: 'InvalidFee', + }, + }, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, owner } = await loadMvFixture(); + await setMinMaxInstantFeeTest( + { vault: manageableVault, owner }, + 10, + 5000, + ); + }); + + it('should fail: when function is paused', async () => { + const { manageableVault, owner } = await loadMvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + encodeFnSelector('setMinMaxInstantFee(uint256,uint256)'), + ); + + await setMinMaxInstantFeeTest( + { vault: manageableVault, owner }, + 0, + 1000, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setMinMaxInstantFee(uint256,uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await setMinMaxInstantFeeTest( + { vault: manageableVault, owner }, + 10, + 5000, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + } = await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setMinMaxInstantFee(uint256,uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await setMinMaxInstantFeeTest( + { vault: manageableVault, owner }, + 10, + 5000, + { from: regularAccounts[0] }, + ); + }); + }); + + describe('setVariabilityTolerance()', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, regularAccounts, owner } = + await loadMvFixture(); + await setVariabilityToleranceTest( + { vault: manageableVault, owner }, + ethers.constants.Zero, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + from: regularAccounts[0], + }, + ); + }); + it('if new value zero', async () => { + const { manageableVault, owner } = await loadMvFixture(); + await setVariabilityToleranceTest( + { vault: manageableVault, owner }, + ethers.constants.Zero, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, owner } = await loadMvFixture(); + await setVariabilityToleranceTest( + { vault: manageableVault, owner }, + 100, + ); + }); + + it('should fail: when function is paused', async () => { + const { manageableVault, owner } = await loadMvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + encodeFnSelector('setVariationTolerance(uint256)'), + ); + + await setVariabilityToleranceTest( + { vault: manageableVault, owner }, + 100, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setVariationTolerance(uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await setVariabilityToleranceTest( + { vault: manageableVault, owner }, + 100, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + } = await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'setVariationTolerance(uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await setVariabilityToleranceTest( + { vault: manageableVault, owner }, + 100, + { from: regularAccounts[0] }, + ); + }); + }); + + describe('removePaymentToken()', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, regularAccounts, owner } = + await loadMvFixture(); + await removePaymentTokenTest( + { vault: manageableVault, owner }, + ethers.constants.AddressZero, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when token is not exists', async () => { + const { owner, manageableVault, stableCoins } = await loadMvFixture(); + await removePaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai.address, + { + revertCustomError: { + customErrorName: 'PaymentTokenNotExists', + }, + }, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, stableCoins, owner, dataFeed } = + await loadMvFixture(); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await removePaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai.address, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role and add 3 options on a row', async () => { + const { manageableVault, owner, stableCoins, dataFeed } = + await loadMvFixture(); + + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdt, + dataFeed.address, + 0, + true, + ); + + await removePaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai.address, + ); + await removePaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdc.address, + ); + await removePaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdt.address, + ); + + await removePaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdt.address, + { + revertCustomError: { + customErrorName: 'PaymentTokenNotExists', + }, + }, + ); + }); + + it('should fail: when function is paused', async () => { + const { manageableVault, owner, stableCoins, dataFeed } = + await loadMvFixture(); + + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + encodeFnSelector('removePaymentToken(address)'), + ); + + await removePaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai.address, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + stableCoins, + dataFeed, + } = await loadMvFixture(); + + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'removePaymentToken(address)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await removePaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdc.address, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + stableCoins, + dataFeed, + } = await loadMvFixture(); + + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdt, + dataFeed.address, + 0, + true, + ); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'removePaymentToken(address)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await removePaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdt.address, + { from: regularAccounts[0] }, + ); + }); + }); + + describe('withdrawToken()', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, regularAccounts, owner } = + await loadMvFixture(); + await withdrawTest( + { vault: manageableVault, owner }, + ethers.constants.AddressZero, + 0, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when there is no token in vault', async () => { + const { owner, manageableVault, stableCoins } = await loadMvFixture(); + await withdrawTest( + { vault: manageableVault, owner }, + stableCoins.dai, + 1, + { revertMessage: 'ERC20: transfer amount exceeds balance' }, + ); + }); + + it('call from address with VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, stableCoins, owner } = await loadMvFixture(); + await mintToken(stableCoins.dai, manageableVault, 1); + await withdrawTest( + { vault: manageableVault, owner }, + stableCoins.dai, + 1, + ); + }); + + it('should fail: when function is paused', async () => { + const { manageableVault, stableCoins, owner } = await loadMvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + encodeFnSelector('withdrawToken(address,uint256)'), + ); + + await withdrawTest( + { vault: manageableVault, owner }, + stableCoins.dai, + 1, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + stableCoins, + } = await loadMvFixture(); + + await mintToken(stableCoins.dai, manageableVault, 1); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'withdrawToken(address,uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await withdrawTest( + { vault: manageableVault, owner }, + stableCoins.dai, + 1, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + stableCoins, + } = await loadMvFixture(); + + await mintToken(stableCoins.dai, manageableVault, 1); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'withdrawToken(address,uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await withdrawTest( + { vault: manageableVault, owner }, + stableCoins.dai, + 1, + { from: regularAccounts[0] }, + ); + }); + }); + + describe('freeFromMinAmount()', async () => { + it('should fail: call from address without vault admin role', async () => { + const { manageableVault, regularAccounts } = await loadMvFixture(); + await expect( + manageableVault + .connect(regularAccounts[0]) + .freeFromMinAmount(regularAccounts[1].address, true), + ).to.be.revertedWithCustomError( + manageableVault, + 'NoFunctionPermission', + ); + }); + it('should not fail', async () => { + const { manageableVault, regularAccounts } = await loadMvFixture(); + await expect( + manageableVault.freeFromMinAmount(regularAccounts[0].address, true), + ).to.not.reverted; + + expect( + await manageableVault.isFreeFromMinAmount( + regularAccounts[0].address, + ), + ).to.eq(true); + }); + it('should fail: already in list', async () => { + const { manageableVault, regularAccounts } = await loadMvFixture(); + await expect( + manageableVault.freeFromMinAmount(regularAccounts[0].address, true), + ).to.not.reverted; + + expect( + await manageableVault.isFreeFromMinAmount( + regularAccounts[0].address, + ), + ).to.eq(true); + + await expect( + manageableVault.freeFromMinAmount(regularAccounts[0].address, true), + ).to.be.revertedWithCustomError(manageableVault, 'SameAddressValue'); + }); + + it('should fail: when function is paused', async () => { + const { manageableVault, regularAccounts } = await loadMvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + encodeFnSelector('freeFromMinAmount(address,bool)'), + ); + + await expect( + manageableVault.freeFromMinAmount(regularAccounts[0].address, true), + ).to.be.revertedWithCustomError(manageableVault, 'Paused'); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, manageableVault, regularAccounts } = + await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'freeFromMinAmount(address,bool)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await expect( + manageableVault + .connect(regularAccounts[0]) + .freeFromMinAmount(regularAccounts[2].address, true), + ).to.not.reverted; + + expect( + await manageableVault.isFreeFromMinAmount( + regularAccounts[2].address, + ), + ).to.eq(true); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + } = await loadMvFixture(); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'freeFromMinAmount(address,bool)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await expect( + manageableVault + .connect(regularAccounts[0]) + .freeFromMinAmount(regularAccounts[3].address, true), + ).to.not.reverted; + + expect( + await manageableVault.isFreeFromMinAmount( + regularAccounts[3].address, + ), + ).to.eq(true); + }); + }); + + describe('changeTokenAllowance()', () => { + it('should fail: call from address without VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, regularAccounts, owner } = + await loadMvFixture(); + await changeTokenAllowanceTest( + { vault: manageableVault, owner }, + ethers.constants.AddressZero, + 0, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + from: regularAccounts[0], + }, + ); + }); + it('should fail: token not exist', async () => { + const { manageableVault, owner, stableCoins } = await loadMvFixture(); + await changeTokenAllowanceTest( + { vault: manageableVault, owner }, + stableCoins.dai.address, + 0, + { + revertCustomError: { + customErrorName: 'UnknownPaymentToken', + }, + }, + ); + }); + it('allowance zero', async () => { + const { manageableVault, owner, stableCoins, dataFeed } = + await loadMvFixture(); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await changeTokenAllowanceTest( + { vault: manageableVault, owner }, + stableCoins.dai.address, + 0, + ); + }); + + it('should fail: when function is paused', async () => { + const { manageableVault, owner, stableCoins, dataFeed } = + await loadMvFixture(); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + encodeFnSelector('changeTokenAllowance(address,uint256)'), + ); + + await changeTokenAllowanceTest( + { vault: manageableVault, owner }, + stableCoins.dai.address, + 100000000, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + stableCoins, + dataFeed, + } = await loadMvFixture(); + + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'changeTokenAllowance(address,uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await changeTokenAllowanceTest( + { vault: manageableVault, owner }, + stableCoins.dai.address, + 100000000, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + stableCoins, + dataFeed, + } = await loadMvFixture(); + + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'changeTokenAllowance(address,uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await changeTokenAllowanceTest( + { vault: manageableVault, owner }, + stableCoins.usdc.address, + 100000000, + { from: regularAccounts[0] }, + ); + }); + }); + + describe('changeTokenFee()', () => { + it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, regularAccounts, owner } = + await loadMvFixture(); + await changeTokenFeeTest( + { vault: manageableVault, owner }, + ethers.constants.AddressZero, + 0, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + from: regularAccounts[0], + }, + ); + }); + it('should fail: token not exist', async () => { + const { manageableVault, owner, stableCoins } = await loadMvFixture(); + await changeTokenFeeTest( + { vault: manageableVault, owner }, + stableCoins.dai.address, + 0, + { + revertCustomError: { + customErrorName: 'UnknownPaymentToken', + }, + }, + ); + }); + it('should fail: fee > 100%', async () => { + const { manageableVault, owner, stableCoins, dataFeed } = + await loadMvFixture(); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await changeTokenFeeTest( + { vault: manageableVault, owner }, + stableCoins.dai.address, + 10001, + { + revertCustomError: { + customErrorName: 'InvalidFee', + }, + }, + ); + }); + it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { + const { manageableVault, owner, stableCoins, dataFeed } = + await loadMvFixture(); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await changeTokenFeeTest( + { vault: manageableVault, owner }, + stableCoins.dai.address, + 100, + ); + }); + + it('should fail: when function is paused', async () => { + const { manageableVault, owner, stableCoins, dataFeed } = + await loadMvFixture(); + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await pauseVaultFn( + { pauseManager, owner }, + manageableVault, + encodeFnSelector('changeTokenFee(address,uint256)'), + ); + + await changeTokenFeeTest( + { vault: manageableVault, owner }, + stableCoins.dai.address, + 100, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + stableCoins, + dataFeed, + } = await loadMvFixture(); + + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'changeTokenFee(address,uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole(vaultRole, regularAccounts[0].address), + ).eq(false); + + await changeTokenFeeTest( + { vault: manageableVault, owner }, + stableCoins.dai.address, + 100, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + manageableVault, + regularAccounts, + roles, + stableCoins, + dataFeed, + } = await loadMvFixture(); + + await addPaymentTokenTest( + { vault: manageableVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + + const vaultRole = await manageableVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + vaultRole, + manageableVault.address, + 'changeTokenFee(address,uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.depositVaultAdmin, + regularAccounts[0].address, + ); + + await changeTokenFeeTest( + { vault: manageableVault, owner }, + stableCoins.usdc.address, + 100, + { from: regularAccounts[0] }, + ); + }); + }); + + describe('internal functions', () => { + it('should fail: invalid rounding tokenTransferFromToTester()', async () => { + const { manageableVault, stableCoins, owner } = await loadMvFixture(); + + await mintToken(stableCoins.usdc, owner, 1000); + + await approveBase18(owner, stableCoins.usdc, manageableVault, 1000); + + await expect( + manageableVault.tokenTransferFromToTester( + stableCoins.usdc.address, + owner.address, + manageableVault.address, + parseUnits('999.999999999'), + 8, + ), + ).to.be.revertedWithCustomError(manageableVault, 'InvalidRounding'); + }); + }); + }); + + otherTests(mvFixture); + }); +}; diff --git a/test/unit/suits/redemption-vault.suits.ts b/test/unit/suits/redemption-vault.suits.ts new file mode 100644 index 00000000..d10cc9a3 --- /dev/null +++ b/test/unit/suits/redemption-vault.suits.ts @@ -0,0 +1,14708 @@ +import { + loadFixture, + setBalance, +} from '@nomicfoundation/hardhat-network-helpers'; +import { increase } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time'; +import { + days, + hours, +} from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time/duration'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { BigNumber, constants, Contract } from 'ethers'; +import { parseUnits } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; + +import { + baseInitParamsMv, + manageableVaultSuits, + mvInitializeParamCases, +} from './manageable-vault.suits'; + +import { encodeFnSelector } from '../../../helpers/utils'; +import { + ERC20Mock, + RedemptionVaultTest, + RedemptionVaultTest__factory, + RedemptionVaultWithAaveTest, + RedemptionVaultWithMorphoTest, + RedemptionVaultWithMTokenTest, + RedemptionVaultWithUSTBTest, +} from '../../../typechain-types'; +import { + acErrors, + blackList, + greenList, + setupPermissionRole, +} from '../../common/ac.helpers'; +import { + approveBase18, + InitializeParamCase, + initializeParamsSuits, + InitializeParamsOpt, + mintToken, + pauseVault, + pauseVaultFn, + asyncForEach, + getCurrentBlockTimestamp, +} from '../../common/common.helpers'; +import { + setMinGrowthApr, + setRoundDataGrowth, +} from '../../common/custom-feed-growth.helpers'; +import { setRoundData } from '../../common/data-feed.helpers'; +import { DefaultFixture } from '../../common/fixtures'; +import { + addPaymentTokenTest, + changeTokenAllowanceTest, + removePaymentTokenTest, + removeInstantLimitConfigTest, + setWaivedFeeAccountTest, + setInstantFeeTest, + setInstantLimitConfigTest, + setMinMaxInstantFeeTest, + setMinAmountTest, + setVariabilityToleranceTest, + withdrawTest, + setMaxInstantShareTest, + setSequentialRequestProcessingTest, +} from '../../common/manageable-vault.helpers'; +import { + approveRedeemRequestTest, + bulkRepayLpLoanRequestTest, + cancelLpLoanRequestTest, + redeemInstantTest, + redeemRequestTest, + rejectRedeemRequestTest, + safeBulkApproveRequestTest, + setLoanLpTest, + setLoanRepaymentAddressTest, + setLoanSwapperVaultTest, + expectedHoldbackPartRateFromAvg, + setPreferLoanLiquidityTest, + setLoanAprTest, + setRequestRedeemerTest, + setMaxApproveRequestIdTest, + truncateToTokenDecimals, +} from '../../common/redemption-vault.helpers'; +import { InitializerParamsRv } from '../../common/vault-initializer.helpers'; +import { sanctionUser } from '../../common/with-sanctions-list.helpers'; + +const REDEMPTION_APPROVE_FN_SELECTORS = [ + encodeFnSelector('approveRequest(uint256,uint256,bool)'), + encodeFnSelector('safeApproveRequest(uint256,uint256)'), + encodeFnSelector('safeApproveRequestAvgRate(uint256,uint256)'), + encodeFnSelector('safeBulkApproveRequestAtSavedRate(uint256[])'), + encodeFnSelector('safeBulkApproveRequest(uint256[])'), + encodeFnSelector('safeBulkApproveRequest(uint256[],uint256)'), + encodeFnSelector('safeBulkApproveRequestAvgRate(uint256[])'), + encodeFnSelector('safeBulkApproveRequestAvgRate(uint256[],uint256)'), +] as const; + +let pauseManager: DefaultFixture['pauseManager']; +let owner: DefaultFixture['owner']; + +export const baseInitParamsRv = ( + fixture: DefaultFixture, +): InitializerParamsRv => ({ + ...baseInitParamsMv(fixture), + requestRedeemer: fixture.requestRedeemer, + loanLp: fixture.loanLp, + loanRepaymentAddress: fixture.loanRepaymentAddress, + redemptionVaultLoanSwapper: fixture.redemptionVaultLoanSwapper, +}); + +export const rvInitializeParamCases: InitializeParamCase[] = + [ + { + title: 'requestRedeemer is zero address', + params: { requestRedeemer: constants.AddressZero }, + revertCustomError: { + customErrorName: 'InvalidAddress', + args: [constants.AddressZero], + }, + }, + ]; + +export const tokensReceiverSelfParamCase = < + TParams extends InitializerParamsRv, +>( + deployUninitialized: RedemptionVaultInitConfig['deployUninitialized'], +): InitializeParamCase => ({ + title: 'tokensReceiver is address(this)', + contract: deployUninitialized, + params: (_, contract) => ({ tokensReceiver: contract!.address }), + revertCustomError: { + customErrorName: 'InvalidAddress', + args: (_, contract) => [contract!.address], + }, +}); + +export type RedemptionVaultInitConfig< + TParams extends InitializerParamsRv = InitializerParamsRv, +> = { + deployUninitialized: ( + fixture: DefaultFixture, + ) => Contract | Promise; + initialize: ( + fixture: DefaultFixture, + params: Partial, + opt?: InitializeParamsOpt, + ) => Promise; + extraParamCases?: InitializeParamCase[]; +}; + +const pauseOtherRedemptionApproveFns = async ( + redemptionVault: Contract, + exceptSelector: (typeof REDEMPTION_APPROVE_FN_SELECTORS)[number], +) => { + await asyncForEach( + REDEMPTION_APPROVE_FN_SELECTORS, + async (selector) => { + if (selector === exceptSelector) { + return; + } + await pauseVaultFn({ pauseManager, owner }, redemptionVault, selector); + }, + true, + ); +}; + +export const redemptionVaultSuits = ( + rvName: string, + rvFixture: () => Promise, + rvConfig: { + createNew: ( + owner: SignerWithAddress, + ) => Promise< + | RedemptionVaultTest + | RedemptionVaultWithAaveTest + | RedemptionVaultWithMTokenTest + | RedemptionVaultWithUSTBTest + | RedemptionVaultWithMorphoTest + >; + key: + | 'redemptionVault' + | 'redemptionVaultWithAave' + | 'redemptionVaultWithMToken' + | 'redemptionVaultWithUSTB' + | 'redemptionVaultWithMorpho'; + }, + deploymentAdditionalChecks: (fixtureRes: DefaultFixture) => Promise, + initConfig: RedemptionVaultInitConfig, + otherTests: (fixture: () => Promise) => void, +) => { + const loadRvFixture = async () => { + const fixture = await loadFixture(rvFixture); + ({ pauseManager, owner } = fixture); + + const { createNew, key } = rvConfig; + return { + ...fixture, + redemptionVault: RedemptionVaultTest__factory.connect( + fixture[key].address, + fixture.owner, + ), + createNew: async () => { + const rv = await createNew(fixture.owner); + return RedemptionVaultTest__factory.connect(rv.address, fixture.owner); + }, + }; + }; + + describe(rvName, function () { + manageableVaultSuits(loadRvFixture, rvConfig, async (fixture) => { + const { redemptionVault, roles } = fixture; + expect(await redemptionVault.contractAdminRole()).eq( + roles.tokenRoles.mTBILL.redemptionVaultAdmin, + ); + }); + + it('deployment', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + loanLp, + loanRepaymentAddress, + redemptionVaultLoanSwapper, + } = fixture; + + expect(await redemptionVault.maxInstantShare()).eq(100_00); + expect(await redemptionVault.loanLp()).eq(loanLp.address); + + expect(await redemptionVault.loanRepaymentAddress()).eq( + loanRepaymentAddress.address, + ); + expect(await redemptionVault.loanSwapperVault()).eq( + redemptionVaultLoanSwapper.address, + ); + + await deploymentAdditionalChecks(fixture); + }); + + describe('initialization', () => { + initializeParamsSuits( + [ + ...mvInitializeParamCases, + ...rvInitializeParamCases, + tokensReceiverSelfParamCase(initConfig.deployUninitialized), + ...(initConfig.extraParamCases ?? []), + ], + loadRvFixture, + initConfig.initialize, + ); + }); + + describe('common', () => { + describe('redeemInstant() complex', () => { + it('should fail: when vault is paused', async () => { + const { + redemptionVault, + owner, + mTBILL, + stableCoins, + regularAccounts, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await pauseVault({ pauseManager, owner }, redemptionVault); + await mintToken(stableCoins.dai, redemptionVault, 100); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('is on pause, but admin can use everything', async () => { + const { + redemptionVault, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await pauseVault({ pauseManager, owner }, redemptionVault); + + await mintToken(stableCoins.dai, redemptionVault, 100); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, stableCoins.dai, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('call for amount == minAmount', async () => { + const { + redemptionVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await mintToken(mTBILL, owner, 100_000); + await mintToken(stableCoins.dai, redemptionVault, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100_000, + ); + }); + + it('redeem 100 mtbill, when price is 5$, 125 mtbill when price is 5.1$, 114 mtbill when price is 5.4$', async () => { + const { + owner, + mockedAggregator, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregatorMToken, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await mintToken(mTBILL, owner, 125); + await mintToken(mTBILL, owner, 114); + + await mintToken(stableCoins.dai, redemptionVault, 1000); + await mintToken(stableCoins.usdc, redemptionVault, 1250); + await mintToken(stableCoins.usdt, redemptionVault, 1140); + + await approveBase18(owner, mTBILL, redemptionVault, 100 + 125 + 114); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.usdt, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setRoundData({ mockedAggregator }, 1.04); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await setRoundData({ mockedAggregator }, 1); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.usdc, + 125, + ); + + await setRoundData({ mockedAggregator }, 1.01); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.usdt, + 114, + ); + }); + }); + + describe('redeemInstant()', () => { + it('should fail: when there is no token in vault', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + revertCustomError: { + customErrorName: 'UnknownPaymentToken', + }, + }, + ); + }); + + it('should fail: when trying to redeem 0 amount', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 0, + { + revertCustomError: { + customErrorName: 'InvalidAmount', + }, + }, + ); + }); + + it('should fail: when function paused', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + } = await loadRvFixture(); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + const selector = encodeFnSelector( + 'redeemInstant(address,uint256,uint256)', + ); + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + selector, + ); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: call with insufficient balance', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: burn amount exceeds balance', + }, + ); + }); + + it('should fail: dataFeed rate 0 ', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mockedAggregator, + mockedAggregatorMToken, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await approveBase18(owner, stableCoins.dai, redemptionVault, 10); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await mintToken(mTBILL, owner, 100_000); + await setRoundData({ mockedAggregator }, 0); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + revertMessage: 'DF: feed is deprecated', + }, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 0); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + revertMessage: 'DF: feed is deprecated', + }, + ); + }); + + it('should fail: call for amount < minAmount', async () => { + const { + redemptionVault, + mockedAggregator, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await mintToken(mTBILL, owner, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 99_999, + { + revertCustomError: { + customErrorName: 'AmountLessThanMin', + }, + }, + ); + }); + + it('should fail: if exceed allowance of deposit by token', async () => { + const { + redemptionVault, + mockedAggregator, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + + await mintToken(mTBILL, owner, 100_000); + await changeTokenAllowanceTest( + { vault: redemptionVault, owner }, + stableCoins.dai.address, + 100, + ); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 99_999, + { + revertCustomError: { + customErrorName: 'AllowanceExceeded', + }, + }, + ); + }); + + it('should fail: if redeem daily limit exceeded', async () => { + const { + redemptionVault, + mockedAggregator, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + + await mintToken(mTBILL, owner, 100_000); + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + 1000, + ); + + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 99_999, + { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + }, + }, + ); + }); + + it('should fail: MV: invalid instant fee when instant fee below min', async () => { + const { + redemptionVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, redemptionVault, 1_000_000); + + await setInstantFeeTest({ vault: redemptionVault, owner }, 100); + await setMinMaxInstantFeeTest( + { vault: redemptionVault, owner }, + 200, + 10_000, + ); + + await mintToken(mTBILL, owner, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'InstantFeeOutOfBounds', + }, + }, + ); + }); + + it('should fail: MV: invalid instant fee when instant fee above max', async () => { + const { + redemptionVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, redemptionVault, 1_000_000); + + await setInstantFeeTest({ vault: redemptionVault, owner }, 5000); + await setMinMaxInstantFeeTest( + { vault: redemptionVault, owner }, + 0, + 2000, + ); + + await mintToken(mTBILL, owner, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'InstantFeeOutOfBounds', + }, + }, + ); + }); + + describe('redeemInstant() multiple instant limits', () => { + it('two windows (12h and 1d): redeem succeeds when under both', async () => { + const { + redemptionVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, redemptionVault, 1_000_000); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: hours(12), limit: parseUnits('1000') }, + ); + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: parseUnits('2000') }, + ); + + await mintToken(mTBILL, owner, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 500, + ); + }); + + it('two windows: redeem fails when one window (12h) is filled', async () => { + const { + redemptionVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, redemptionVault, 1_000_000); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: hours(12), limit: parseUnits('100') }, + ); + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: parseUnits('10000') }, + ); + + await mintToken(mTBILL, owner, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 60, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + }, + }, + ); + }); + + it('two windows: 12h epoch resets after 12h, redeem succeeds again', async () => { + const { + redemptionVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, redemptionVault, 1_000_000); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: hours(12), limit: parseUnits('100') }, + ); + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: parseUnits('10000') }, + ); + + await mintToken(mTBILL, owner, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await increase(hours(12)); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 10, + ); + }); + + it('two windows: 1d epoch resets after 1d, redeem succeeds again', async () => { + const { + redemptionVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, redemptionVault, 1_000_000); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: hours(12), limit: parseUnits('100000') }, + ); + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: parseUnits('100') }, + ); + + await mintToken(mTBILL, owner, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await increase(days(1)); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + }); + + it('four windows: redeem fails when one limit is exceeded', async () => { + const { + redemptionVault, + mockedAggregator, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: hours(1), limit: parseUnits('50') }, + ); + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: hours(6), limit: parseUnits('500') }, + ); + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: hours(12), limit: parseUnits('5000') }, + ); + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: parseUnits('50000') }, + ); + + await mintToken(mTBILL, owner, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 60, + { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + }, + }, + ); + }); + + it('four windows: redeem succeeds when under all limits', async () => { + const { + redemptionVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, redemptionVault, 1_000_000); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: hours(1), limit: parseUnits('100') }, + ); + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: hours(6), limit: parseUnits('500') }, + ); + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: hours(12), limit: parseUnits('1000') }, + ); + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: parseUnits('5000') }, + ); + + await mintToken(mTBILL, owner, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + }); + + it('remove window and add same window again: usage resets', async () => { + const { + redemptionVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, redemptionVault, 1_000_000); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: parseUnits('1000') }, + ); + + await mintToken(mTBILL, owner, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 400, + ); + + await removeInstantLimitConfigTest( + { vault: redemptionVault, owner }, + days(1), + ); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: parseUnits('1000') }, + ); + + const statuses = await redemptionVault.getInstantLimitStatuses(); + + expect(statuses[0].remaining).eq(parseUnits('1000')); + expect(statuses[0].lastUpdated).not.eq(0); + expect(statuses[0].window).eq(days(1)); + expect(statuses[0].limit).eq(parseUnits('1000')); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 700, + ); + }); + + it('remove window: later redeem is not capped by removed limit', async () => { + const { + redemptionVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, redemptionVault, 1_000_000); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: parseUnits('100') }, + ); + + await mintToken(mTBILL, owner, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 80, + ); + + await removeInstantLimitConfigTest( + { vault: redemptionVault, owner }, + days(1), + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 500, + ); + }); + }); + + describe('redeemInstant() sliding rate limit (RateLimitLibrary)', () => { + const setupRedeemInstantRateLimitFixture = async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + } = fixture; + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await mintToken(stableCoins.dai, redemptionVault, 1_000_000); + await mintToken(mTBILL, owner, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + return fixture; + }; + + it('10h window: full consume, after 1h restores ~10% and two redeems use it', async () => { + const { + redemptionVault, + owner, + mTBILL, + stableCoins, + mTokenToUsdDataFeed, + } = await setupRedeemInstantRateLimitFixture(); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: hours(10), limit: parseUnits('1000') }, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1000, + ); + + await increase(hours(1)); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('1d window: after 80% consumed and limit halved, redeem fails', async () => { + const { + redemptionVault, + owner, + mTBILL, + stableCoins, + mTokenToUsdDataFeed, + } = await setupRedeemInstantRateLimitFixture(); + + const initialLimit = parseUnits('1000'); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: initialLimit }, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 800, + ); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: initialLimit.div(2) }, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + }, + }, + ); + }); + + it('1d window: after limit halved, wait 12h and redeem small amount', async () => { + const { + redemptionVault, + owner, + mTBILL, + stableCoins, + mTokenToUsdDataFeed, + } = await setupRedeemInstantRateLimitFixture(); + + const initialLimit = parseUnits('1000'); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: initialLimit }, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 800, + ); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: initialLimit.div(2) }, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + }, + }, + ); + + await increase(hours(18)); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + ); + }); + + it('multiple windows active at the same time', async () => { + const { + redemptionVault, + owner, + mTBILL, + stableCoins, + mTokenToUsdDataFeed, + } = await setupRedeemInstantRateLimitFixture(); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: hours(1), limit: parseUnits('100') }, + ); + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: hours(6), limit: parseUnits('500') }, + ); + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: parseUnits('10000') }, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + { + revertCustomError: { + customErrorName: 'WindowLimitExceeded', + }, + }, + ); + + await increase(hours(1)); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 50, + ); + }); + }); + + it('should fail: if min receive amount greater then actual', async () => { + const { + redemptionVault, + mockedAggregator, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + + await mintToken(mTBILL, owner, 100_000); + + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + minAmount: parseUnits('1000000'), + }, + stableCoins.dai, + 99_999, + { + revertCustomError: { + customErrorName: 'SlippageExceeded', + }, + }, + ); + }); + + it('should fail: if some fee = 100%', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 10000, + true, + ); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'FeeExceedsAmount', + }, + }, + ); + + await removePaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setInstantFeeTest({ vault: redemptionVault, owner }, 10000); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'FeeExceedsAmount', + }, + }, + ); + }); + + it('should fail: greenlist enabled and user not in greenlist ', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await redemptionVault.setGreenlistEnable(true); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + revertCustomError: { + customErrorName: 'NotGreenlisted', + }, + }, + ); + }); + + it('should fail: user in blacklist ', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + blackListableTester, + accessControl, + regularAccounts, + } = await loadRvFixture(); + + await blackList( + { blacklistable: blackListableTester, accessControl, owner }, + regularAccounts[0], + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_BLACKLISTED, + }, + ); + }); + + it('should fail: user in sanctions list', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + mockedSanctionsList, + } = await loadRvFixture(); + + await sanctionUser( + { sanctionsList: mockedSanctionsList }, + regularAccounts[0], + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Sanctioned', + }, + }, + ); + }); + + it('should fail: when function with custom recipient is paused', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + customRecipient, + } = await loadRvFixture(); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + const selector = encodeFnSelector( + 'redeemInstant(address,uint256,uint256,address)', + ); + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + selector, + ); + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + greenListableTester, + accessControl, + customRecipient, + } = await loadRvFixture(); + + await redemptionVault.setGreenlistEnable(true); + + await greenList( + { greenlistable: greenListableTester, accessControl, owner }, + owner, + ); + + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 1, + { + revertCustomError: { + customErrorName: 'NotGreenlisted', + }, + }, + ); + }); + + it('should fail: recipient in blacklist (custom recipient overload)', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + blackListableTester, + accessControl, + regularAccounts, + customRecipient, + } = await loadRvFixture(); + + await blackList( + { blacklistable: blackListableTester, accessControl, owner }, + customRecipient, + ); + + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_BLACKLISTED, + }, + ); + }); + + it('should fail: recipient in sanctions list (custom recipient overload)', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + mockedSanctionsList, + customRecipient, + } = await loadRvFixture(); + + await sanctionUser( + { sanctionsList: mockedSanctionsList }, + customRecipient, + ); + + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Sanctioned', + }, + }, + ); + }); + + it('when enough liquidity on vault but not on loan lp', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await mintToken(stableCoins.dai, redemptionVault, 1000); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + describe('loan lp', () => { + describe('preferLoanLiquidity=false', () => { + it('when enough liquidity on loan lp but not on vault', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + loanLp, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await mintToken(mTokenLoan, loanLp, 1000); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 1000); + await mintToken( + stableCoins.dai, + redemptionVaultLoanSwapper, + 1000, + ); + + await approveBase18( + loanLp, + stableCoins.dai, + redemptionVault, + 1000, + ); + await withdrawTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + await stableCoins.dai.balanceOf(redemptionVault.address), + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('when enough liquidity on loan lp and on vault but lp liquidity should be untouched', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + loanLp, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await mintToken(mTokenLoan, loanLp, 1000); + await mintToken(stableCoins.dai, redemptionVault, 1000); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 1000); + await mintToken( + stableCoins.dai, + redemptionVaultLoanSwapper, + 1000, + ); + + await approveBase18( + loanLp, + stableCoins.dai, + redemptionVault, + 1000, + ); + await withdrawTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + await stableCoins.dai.balanceOf(redemptionVault.address), + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('when 25% of liquidity on vault and 75% on loan lp', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + loanLp, + mockedAggregatorMToken, + mockedAggregator, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await mintToken(stableCoins.dai, redemptionVault, 25); + + await mintToken(mTokenLoan, loanLp, 75); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 75); + await mintToken(stableCoins.dai, redemptionVaultLoanSwapper, 75); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 75); + + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 1, + ); + await setRoundData({ mockedAggregator }, 1); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('when 25% of liquidity on vault and 75% on loan lp and all the fees are 0%', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + loanLp, + mockedAggregatorMToken, + mockedAggregator, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await mintToken(stableCoins.dai, redemptionVault, 25); + + await mintToken(mTokenLoan, loanLp, 75); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 75); + await mintToken(stableCoins.dai, redemptionVaultLoanSwapper, 75); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 75); + + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 1, + ); + await setRoundData({ mockedAggregator }, 1); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setInstantFeeTest({ vault: redemptionVault, owner }, 0); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('should fail: when not enough liquidity on both vault and loan lp', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + loanLp, + mTokenLoan, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 1000); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('should fail: when not enough liquidity on vault and loan lp is not set', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await setLoanLpTest( + { redemptionVault, owner }, + ethers.constants.AddressZero, + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('should fail: when not enough liquidity on vault and loan swapper is not set', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await setLoanSwapperVaultTest( + { redemptionVault, owner }, + ethers.constants.AddressZero, + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('should fail: when not enough liquidity on vault and loan swapper and loanLp are not set', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await setLoanSwapperVaultTest( + { redemptionVault, owner }, + ethers.constants.AddressZero, + ); + await setLoanLpTest( + { redemptionVault, owner }, + ethers.constants.AddressZero, + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('should fail: when not fee waived and vault does not have enough liquidity', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + redemptionVaultLoanSwapper, + loanLp, + mTokenLoan, + mockedAggregatorMToken, + mockedAggregator, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + + await mintToken(mTokenLoan, loanLp, 100); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 100); + await mintToken(stableCoins.dai, redemptionVaultLoanSwapper, 100); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 100); + + await setWaivedFeeAccountTest( + { vault: redemptionVaultLoanSwapper, owner }, + redemptionVault.address, + false, + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 1, + ); + await setRoundData({ mockedAggregator }, 1); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + }); + + describe('preferLoanLiquidity=true', () => { + it('when enough liquidity on loan lp but not on vault', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + loanLp, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await mintToken(mTokenLoan, loanLp, 1000); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 1000); + await mintToken( + stableCoins.dai, + redemptionVaultLoanSwapper, + 1000, + ); + + await approveBase18( + loanLp, + stableCoins.dai, + redemptionVault, + 1000, + ); + await withdrawTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + await stableCoins.dai.balanceOf(redemptionVault.address), + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setPreferLoanLiquidityTest( + { redemptionVault, owner }, + true, + ); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('when enough liquidity on loan lp and on vault but vault liquidity should be untouched', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + loanLp, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await mintToken(mTokenLoan, loanLp, 1000); + await mintToken(stableCoins.dai, redemptionVault, 1000); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 1000); + await mintToken( + stableCoins.dai, + redemptionVaultLoanSwapper, + 1000, + ); + + await approveBase18( + loanLp, + stableCoins.dai, + redemptionVault, + 1000, + ); + await withdrawTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + await stableCoins.dai.balanceOf(redemptionVault.address), + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setPreferLoanLiquidityTest( + { redemptionVault, owner }, + true, + ); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('when 25% of liquidity on vault and 75% on loan lp', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + loanLp, + mockedAggregatorMToken, + mockedAggregator, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await mintToken(stableCoins.dai, redemptionVault, 25); + + await mintToken(mTokenLoan, loanLp, 75); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 75); + await mintToken(stableCoins.dai, redemptionVaultLoanSwapper, 75); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 75); + + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 1, + ); + await setRoundData({ mockedAggregator }, 1); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setPreferLoanLiquidityTest( + { redemptionVault, owner }, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('when 25% of liquidity on vault and 75% on loan lp and all the fees are 0%', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + loanLp, + mockedAggregatorMToken, + mockedAggregator, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await mintToken(stableCoins.dai, redemptionVault, 25); + + await mintToken(mTokenLoan, loanLp, 75); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 75); + await mintToken(stableCoins.dai, redemptionVaultLoanSwapper, 75); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 75); + + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 1, + ); + await setRoundData({ mockedAggregator }, 1); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setInstantFeeTest({ vault: redemptionVault, owner }, 0); + await setPreferLoanLiquidityTest( + { redemptionVault, owner }, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('should fail: when not enough liquidity on both vault and loan lp', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + loanLp, + mTokenLoan, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 1000); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setPreferLoanLiquidityTest( + { redemptionVault, owner }, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('should fail: when not enough liquidity on vault and loan lp is not set', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await setLoanLpTest( + { redemptionVault, owner }, + ethers.constants.AddressZero, + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setPreferLoanLiquidityTest( + { redemptionVault, owner }, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('should fail: when not enough liquidity on vault and loan swapper is not set', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await setLoanSwapperVaultTest( + { redemptionVault, owner }, + ethers.constants.AddressZero, + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setPreferLoanLiquidityTest( + { redemptionVault, owner }, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('should fail: when not enough liquidity on vault and loan swapper and loanLp are not set', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await setLoanSwapperVaultTest( + { redemptionVault, owner }, + ethers.constants.AddressZero, + ); + await setLoanLpTest( + { redemptionVault, owner }, + ethers.constants.AddressZero, + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setPreferLoanLiquidityTest( + { redemptionVault, owner }, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('should skip loan liquidity and fallback to vault liquidity when not fee waived', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + loanLp, + mTokenLoan, + redemptionVaultLoanSwapper, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await mintToken(mTokenLoan, loanLp, 1000); + await mintToken(stableCoins.dai, redemptionVault, 1000); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 1000); + await mintToken( + stableCoins.dai, + redemptionVaultLoanSwapper, + 1000, + ); + + await setWaivedFeeAccountTest( + { vault: redemptionVaultLoanSwapper, owner }, + redemptionVault.address, + false, + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setPreferLoanLiquidityTest( + { redemptionVault, owner }, + true, + ); + + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + loanLiquidityExpectToFail: true, + }, + stableCoins.dai, + 100, + ); + }); + + it('should fail: when not fee waived and vault does not have enough liquidity', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + redemptionVaultLoanSwapper, + loanLp, + mTokenLoan, + mockedAggregatorMToken, + mockedAggregator, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + + await mintToken(mTokenLoan, loanLp, 100); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 100); + await mintToken(stableCoins.dai, redemptionVaultLoanSwapper, 100); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 100); + + await setWaivedFeeAccountTest( + { vault: redemptionVaultLoanSwapper, owner }, + redemptionVault.address, + false, + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 1, + ); + await setRoundData({ mockedAggregator }, 1); + await setPreferLoanLiquidityTest( + { redemptionVault, owner }, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('should fail: when swapper fails it should catch the error and fail later during the transfer', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + redemptionVaultLoanSwapper, + loanLp, + mTokenLoan, + mockedAggregatorMToken, + mockedAggregatorMTokenLoan, + mockedAggregator, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + + await mintToken(mTokenLoan, loanLp, 100); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 100); + await mintToken(stableCoins.dai, redemptionVaultLoanSwapper, 100); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 100); + + await setWaivedFeeAccountTest( + { vault: redemptionVaultLoanSwapper, owner }, + redemptionVault.address, + false, + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 1, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData( + { mockedAggregator: mockedAggregatorMTokenLoan }, + 0, + ); + + await setPreferLoanLiquidityTest( + { redemptionVault, owner }, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + }); + }); + + describe('holdback', () => { + it('when max instant share is 100%', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await mintToken(stableCoins.dai, redemptionVault, 1000); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('should fail: when max instant share is not 100%', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await mintToken(stableCoins.dai, redemptionVault, 1000); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setMaxInstantShareTest( + { vault: redemptionVault, owner }, + 90_00, + ); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'InstantShareTooHigh', + }, + }, + ); + }); + }); + + it('redeem 100 mTBILL when 10% growth is applied', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + customFeedGrowth, + } = await loadRvFixture(); + + await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); + await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, 10); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + expectedAmountOut: parseUnits('99.000314820', 9), + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('redeem 100 mTBILL when -10% growth is applied', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + customFeedGrowth, + } = await loadRvFixture(); + + await setMinGrowthApr({ owner, customFeedGrowth }, -10); + await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); + await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, -10); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + expectedAmountOut: parseUnits('98.999685180', 9), + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('redeem 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('redeem 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('redeem 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await redemptionVault.freeFromMinAmount(owner.address, true); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('redeem 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setWaivedFeeAccountTest( + { vault: redemptionVault, owner }, + owner.address, + true, + ); + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + waivedFee: true, + }, + stableCoins.dai, + 100, + ); + }); + + it('redeem 100 mTBILL (custom recipient overload)', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + customRecipient, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('redeem 100 mTBILL when recipient == msg.sender (custom recipient overload)', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient: regularAccounts[0], + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('redeem 100 mTBILL when other fn overload is paused (custom recipient overload)', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + customRecipient, + } = await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('redeemInstant(address,uint256,uint256)'), + ); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when user didnt approve mTokens to redeem', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + } = await loadRvFixture(); + + await mintToken(mTBILL, regularAccounts[0], 10); + await mintToken(stableCoins.dai, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 10, + { + from: regularAccounts[0], + }, + ); + }); + + it('redeem 100 mTBILL when other fn overload is paused', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('redeemInstant(address,uint256,uint256,address)'), + ); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + }); + + describe('setLoanLp()', () => { + it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { + const { redemptionVault, regularAccounts, owner } = await loadFixture( + rvFixture, + ); + await setLoanLpTest( + { redemptionVault, owner }, + ethers.constants.AddressZero, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + from: regularAccounts[0], + }, + ); + }); + it('if new loanLp address zero', async () => { + const { redemptionVault, owner } = await loadRvFixture(); + await setLoanLpTest( + { redemptionVault, owner }, + ethers.constants.AddressZero, + ); + }); + + it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { + const { redemptionVault, owner } = await loadRvFixture(); + await setLoanLpTest({ redemptionVault, owner }, owner.address); + }); + + it('should fail: when function is paused', async () => { + const { redemptionVault, owner } = await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('setLoanLp(address)'), + ); + + await setLoanLpTest({ redemptionVault, owner }, owner.address, { + revertCustomError: { + customErrorName: 'Paused', + }, + }); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, redemptionVault, regularAccounts } = + await loadRvFixture(); + + const contractAdminRole = await redemptionVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + redemptionVault.address, + 'setLoanLp(address)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole( + contractAdminRole, + regularAccounts[0].address, + ), + ).eq(false); + + await setLoanLpTest( + { redemptionVault, owner }, + regularAccounts[1].address, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + redemptionVault, + regularAccounts, + roles, + } = await loadRvFixture(); + + const contractAdminRole = await redemptionVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + redemptionVault.address, + 'setLoanLp(address)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.redemptionVaultAdmin, + regularAccounts[0].address, + ); + + await setLoanLpTest( + { redemptionVault, owner }, + regularAccounts[2].address, + { from: regularAccounts[0] }, + ); + }); + }); + + describe('setLoanRepaymentAddress()', () => { + it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { + const { redemptionVault, regularAccounts, owner } = await loadFixture( + rvFixture, + ); + await setLoanRepaymentAddressTest( + { redemptionVault, owner }, + regularAccounts[0].address, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { + const { redemptionVault, owner, regularAccounts } = + await loadRvFixture(); + await setLoanRepaymentAddressTest( + { redemptionVault, owner }, + regularAccounts[0].address, + ); + }); + + it('should fail: when function is paused', async () => { + const { redemptionVault, owner, regularAccounts } = + await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('setLoanRepaymentAddress(address)'), + ); + + await setLoanRepaymentAddressTest( + { redemptionVault, owner }, + regularAccounts[0].address, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, redemptionVault, regularAccounts } = + await loadRvFixture(); + + const contractAdminRole = await redemptionVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + redemptionVault.address, + 'setLoanRepaymentAddress(address)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole( + contractAdminRole, + regularAccounts[0].address, + ), + ).eq(false); + + await setLoanRepaymentAddressTest( + { redemptionVault, owner }, + regularAccounts[1].address, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + redemptionVault, + regularAccounts, + roles, + } = await loadRvFixture(); + + const contractAdminRole = await redemptionVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + redemptionVault.address, + 'setLoanRepaymentAddress(address)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.redemptionVaultAdmin, + regularAccounts[0].address, + ); + + await setLoanRepaymentAddressTest( + { redemptionVault, owner }, + regularAccounts[2].address, + { from: regularAccounts[0] }, + ); + }); + }); + + describe('setLoanSwapperVault()', () => { + it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { + const { redemptionVault, regularAccounts, owner } = await loadFixture( + rvFixture, + ); + await setLoanSwapperVaultTest( + { redemptionVault, owner }, + regularAccounts[0].address, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { + const { redemptionVault, owner, regularAccounts } = + await loadRvFixture(); + await setLoanSwapperVaultTest( + { redemptionVault, owner }, + regularAccounts[0].address, + ); + }); + + it('should fail: when function is paused', async () => { + const { redemptionVault, owner, regularAccounts } = + await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('setLoanSwapperVault(address)'), + ); + + await setLoanSwapperVaultTest( + { redemptionVault, owner }, + regularAccounts[0].address, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, redemptionVault, regularAccounts } = + await loadRvFixture(); + + const contractAdminRole = await redemptionVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + redemptionVault.address, + 'setLoanSwapperVault(address)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole( + contractAdminRole, + regularAccounts[0].address, + ), + ).eq(false); + + await setLoanSwapperVaultTest( + { redemptionVault, owner }, + regularAccounts[1].address, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + redemptionVault, + regularAccounts, + roles, + } = await loadRvFixture(); + + const contractAdminRole = await redemptionVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + redemptionVault.address, + 'setLoanSwapperVault(address)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.redemptionVaultAdmin, + regularAccounts[0].address, + ); + + await setLoanSwapperVaultTest( + { redemptionVault, owner }, + regularAccounts[2].address, + { from: regularAccounts[0] }, + ); + }); + }); + + describe('setPreferLoanLiquidity()', () => { + it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { + const { redemptionVault, regularAccounts, owner } = await loadFixture( + rvFixture, + ); + await setPreferLoanLiquidityTest({ redemptionVault, owner }, true, { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }); + }); + + it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { + const { redemptionVault, owner } = await loadRvFixture(); + await setPreferLoanLiquidityTest({ redemptionVault, owner }, true); + }); + + it('should fail: when function is paused', async () => { + const { redemptionVault, owner } = await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('setPreferLoanLiquidity(bool)'), + ); + + await setPreferLoanLiquidityTest({ redemptionVault, owner }, true, { + revertCustomError: { + customErrorName: 'Paused', + }, + }); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, redemptionVault, regularAccounts } = + await loadRvFixture(); + + const contractAdminRole = await redemptionVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + redemptionVault.address, + 'setPreferLoanLiquidity(bool)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole( + contractAdminRole, + regularAccounts[0].address, + ), + ).eq(false); + + await setPreferLoanLiquidityTest({ redemptionVault, owner }, true, { + from: regularAccounts[0], + }); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + redemptionVault, + regularAccounts, + roles, + } = await loadRvFixture(); + + const contractAdminRole = await redemptionVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + redemptionVault.address, + 'setPreferLoanLiquidity(bool)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.redemptionVaultAdmin, + regularAccounts[0].address, + ); + + await setPreferLoanLiquidityTest({ redemptionVault, owner }, true, { + from: regularAccounts[0], + }); + }); + }); + + describe('setRequestRedeemer()', () => { + it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { + const { redemptionVault, regularAccounts, owner } = + await loadRvFixture(); + + await setRequestRedeemerTest( + { redemptionVault, owner }, + regularAccounts[1].address, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: if redeemer is zero address', async () => { + const { redemptionVault, owner } = await loadRvFixture(); + + await setRequestRedeemerTest( + { redemptionVault, owner }, + constants.AddressZero, + { + revertCustomError: { + customErrorName: 'InvalidAddress', + args: [constants.AddressZero], + }, + }, + ); + }); + + it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { + const { redemptionVault, owner, regularAccounts } = + await loadRvFixture(); + + await setRequestRedeemerTest( + { redemptionVault, owner }, + regularAccounts[1].address, + ); + }); + + it('should fail: when function is paused', async () => { + const { redemptionVault, owner, regularAccounts } = + await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('setRequestRedeemer(address)'), + ); + + await setRequestRedeemerTest( + { redemptionVault, owner }, + regularAccounts[1].address, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, redemptionVault, regularAccounts } = + await loadRvFixture(); + + const contractAdminRole = await redemptionVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + redemptionVault.address, + 'setRequestRedeemer(address)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole( + contractAdminRole, + regularAccounts[0].address, + ), + ).eq(false); + + await setRequestRedeemerTest( + { redemptionVault, owner }, + regularAccounts[1].address, + { from: regularAccounts[0] }, + ); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + redemptionVault, + regularAccounts, + roles, + } = await loadRvFixture(); + + const contractAdminRole = await redemptionVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + redemptionVault.address, + 'setRequestRedeemer(address)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.redemptionVaultAdmin, + regularAccounts[0].address, + ); + + await setRequestRedeemerTest( + { redemptionVault, owner }, + regularAccounts[2].address, + { from: regularAccounts[0] }, + ); + }); + }); + + describe('setLoanApr()', () => { + it('should fail: call from address without REDEMPTION_VAULT_ADMIN_ROLE role', async () => { + const { redemptionVault, regularAccounts, owner } = + await loadRvFixture(); + + await setLoanAprTest({ redemptionVault, owner }, 5000, { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }); + }); + + it('call from address with REDEMPTION_VAULT_ADMIN_ROLE role', async () => { + const { redemptionVault, owner } = await loadRvFixture(); + await setLoanAprTest({ redemptionVault, owner }, 5000); + }); + + it('should fail: when function is paused', async () => { + const { redemptionVault, owner } = await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('setLoanApr(uint256)'), + ); + + await setLoanAprTest({ redemptionVault, owner }, 5000, { + revertCustomError: { + customErrorName: 'Paused', + }, + }); + }); + + it('succeeds with only scoped function permission', async () => { + const { accessControl, owner, redemptionVault, regularAccounts } = + await loadRvFixture(); + + const contractAdminRole = await redemptionVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + redemptionVault.address, + 'setLoanApr(uint256)', + regularAccounts[0].address, + ); + + expect( + await accessControl.hasRole( + contractAdminRole, + regularAccounts[0].address, + ), + ).eq(false); + + await setLoanAprTest({ redemptionVault, owner }, 5000, { + from: regularAccounts[0], + }); + }); + + it('succeeds with scoped permission and vault admin role', async () => { + const { + accessControl, + owner, + redemptionVault, + regularAccounts, + roles, + } = await loadRvFixture(); + + const contractAdminRole = await redemptionVault.contractAdminRole(); + await setupPermissionRole( + { accessControl, owner }, + contractAdminRole, + redemptionVault.address, + 'setLoanApr(uint256)', + regularAccounts[0].address, + ); + + await accessControl['grantRole(bytes32,address)']( + roles.tokenRoles.mTBILL.redemptionVaultAdmin, + regularAccounts[0].address, + ); + + await setLoanAprTest({ redemptionVault, owner }, 7500, { + from: regularAccounts[0], + }); + }); + }); + + describe('redeemRequest()', () => { + describe('holdback', () => { + it('when 40% instant and 60% holdback', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + mTBILL, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 40_00, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when 90% instant and 10% holdback', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + mTBILL, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 90_00, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when 0% instant and 100% holdback', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + mTBILL, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 0, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when 50% instant and 50% holdback and request recipient is different from msg.sender', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + mTBILL, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 0, + customRecipient: regularAccounts[1], + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when 50% instant and 50% holdback and instant recipient is different from msg.sender', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + mTBILL, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 0, + customRecipientInstant: regularAccounts[1], + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('when 50% instant and 50% holdback and request and instant recipients are different from msg.sender and from each other', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + mTBILL, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 0, + customRecipient: regularAccounts[1], + customRecipientInstant: regularAccounts[2], + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('should fail: when 100% instant and 0% holdback', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + mTBILL, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 100_00, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'InvalidAmount', + }, + }, + ); + }); + + it('should fail: when instant share exceeds max instant share', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + mTBILL, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setMaxInstantShareTest( + { vault: redemptionVault, owner }, + 80_00, + ); + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 90_00, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'InstantShareTooHigh', + }, + }, + ); + }); + }); + + it('should fail: when there is no token in vault', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + revertCustomError: { + customErrorName: 'UnknownPaymentToken', + }, + }, + ); + }); + + it('should fail: when trying to redeem 0 amount', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 0, + { + revertCustomError: { + customErrorName: 'InvalidAmount', + }, + }, + ); + }); + + it('should fail: when function paused', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + } = await loadRvFixture(); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + const selector = encodeFnSelector('redeemRequest(address,uint256)'); + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + selector, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: call with insufficient allowance', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: insufficient allowance', + }, + ); + }); + + it('should fail: call with insufficient balance', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('should fail: dataFeed rate 0 ', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mockedAggregator, + mockedAggregatorMToken, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await approveBase18(owner, stableCoins.dai, redemptionVault, 10); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await mintToken(mTBILL, owner, 100_000); + await setRoundData({ mockedAggregator }, 0); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + revertMessage: 'DF: feed is deprecated', + }, + ); + await setRoundData({ mockedAggregator }, 1); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 0); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + revertMessage: 'DF: feed is deprecated', + }, + ); + }); + + it('should fail: call for amount < minAmount', async () => { + const { + redemptionVault, + mockedAggregator, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await mintToken(mTBILL, owner, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 99_999, + { + revertCustomError: { + customErrorName: 'AmountLessThanMin', + }, + }, + ); + }); + + it('should fail: greenlist enabled and user not in greenlist ', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await redemptionVault.setGreenlistEnable(true); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + revertCustomError: { + customErrorName: 'NotGreenlisted', + }, + }, + ); + }); + + it('should fail: user in blacklist ', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + blackListableTester, + accessControl, + regularAccounts, + } = await loadRvFixture(); + + await blackList( + { blacklistable: blackListableTester, accessControl, owner }, + regularAccounts[0], + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_BLACKLISTED, + }, + ); + }); + + it('should fail: user in sanctions list', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + mockedSanctionsList, + } = await loadRvFixture(); + + await sanctionUser( + { sanctionsList: mockedSanctionsList }, + regularAccounts[0], + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Sanctioned', + }, + }, + ); + }); + + it('should fail: when function paused (custom recipient overload)', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + regularAccounts, + customRecipient, + } = await loadRvFixture(); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + const selector = encodeFnSelector( + 'redeemRequest(address,uint256,address,uint256,uint256,address)', + ); + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + selector, + ); + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: greenlist enabled and recipient not in greenlist (custom recipient overload)', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + greenListableTester, + accessControl, + customRecipient, + } = await loadRvFixture(); + + await redemptionVault.setGreenlistEnable(true); + + await greenList( + { greenlistable: greenListableTester, accessControl, owner }, + owner, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 1, + { + revertCustomError: { + customErrorName: 'NotGreenlisted', + }, + }, + ); + }); + + it('should fail: recipient in blacklist (custom recipient overload)', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + blackListableTester, + accessControl, + regularAccounts, + customRecipient, + } = await loadRvFixture(); + + await blackList( + { blacklistable: blackListableTester, accessControl, owner }, + customRecipient, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_BLACKLISTED, + }, + ); + }); + + it('should fail: recipient in sanctions list (custom recipient overload)', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + mockedSanctionsList, + customRecipient, + } = await loadRvFixture(); + + await sanctionUser( + { sanctionsList: mockedSanctionsList }, + customRecipient, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 1, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Sanctioned', + }, + }, + ); + }); + + it('redeem request 100 mTBILL, greenlist enabled and user in greenlist ', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + greenListableTester, + mTokenToUsdDataFeed, + accessControl, + regularAccounts, + dataFeed, + } = await loadRvFixture(); + + await redemptionVault.setGreenlistEnable(true); + + await greenList( + { greenlistable: greenListableTester, accessControl, owner }, + regularAccounts[0], + ); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('instant limit configs are not applied', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + mockedAggregator, + } = await loadRvFixture(); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 4); + + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: hours(12), limit: parseUnits('100') }, + ); + await setInstantLimitConfigTest( + { vault: redemptionVault, owner }, + { window: days(1), limit: parseUnits('100') }, + ); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 500); + await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 500); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 500, + { from: regularAccounts[0] }, + ); + }); + + it('redeem request with 10% growth is applied', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + customFeedGrowth, + } = await loadRvFixture(); + + await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); + await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, 10); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('redeem request with -10% growth is applied', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + customFeedGrowth, + } = await loadRvFixture(); + + await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); + await setMinGrowthApr({ owner, customFeedGrowth }, -10); + await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, -10); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and token fee 1%', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ without checking of minDepositAmount', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await redemptionVault.freeFromMinAmount(owner.address, true); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }); + + it('redeem request 100 mTBILL, when price of stable is 1.03$ and mToken price is 5$ and user in waivedFeeRestriction', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setWaivedFeeAccountTest( + { vault: redemptionVault, owner }, + owner.address, + true, + ); + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + waivedFee: true, + }, + stableCoins.dai, + 100, + ); + }); + it('redeem request 100 mTBILL (custom recipient overload)', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + customRecipient, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('redeem request 100 mTBILL when recipient == msg.sender (custom recipient overload)', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient: regularAccounts[0], + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('redeem request 100 mTBILL when other fn overload is paused (custom recipient overload)', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + customRecipient, + } = await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('redeemRequest(address,uint256)'), + ); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + + it('redeem request 100 mTBILL when other fn overload is paused', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + } = await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector( + 'redeemRequest(address,uint256,address,uint256,uint256,address)', + ), + ); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18(regularAccounts[0], mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + }, + ); + }); + }); + + describe('approveRequest()', async () => { + it('should fail: when request id exceeds maxApproveRequestId', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await setMaxApproveRequestIdTest({ redemptionVault, owner }, 0); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 1, + parseUnits('5'), + { + revertCustomError: { + customErrorName: 'RequestIdTooHigh', + args: [1, 0], + }, + }, + ); + }); + + describe('avgRate=false', async () => { + it('should fail: call from address without vault admin role', async () => { + const { + redemptionVault, + regularAccounts, + mTokenToUsdDataFeed, + mTBILL, + } = await loadRvFixture(); + await approveRedeemRequestTest( + { + redemptionVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + }, + 1, + parseUnits('1'), + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: when function is paused', async () => { + const { owner, redemptionVault, mTBILL, mTokenToUsdDataFeed } = + await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('approveRequest(uint256,uint256,bool)'), + ); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: if some fee = 100%', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 10000, + true, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'FeeExceedsAmount', + }, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 1, + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + describe('request addresses access', () => { + const setupPendingRedeemRequest = async ( + fixture: Awaited>, + opts?: { + customRecipient?: SignerWithAddress; + customClaimer?: SignerWithAddress; + }, + ) => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + dataFeed, + requestRedeemer, + mockedAggregator, + mockedAggregatorMToken, + } = fixture; + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + mTBILL, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData( + { mockedAggregator: mockedAggregatorMToken }, + 5, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient: opts?.customRecipient ?? regularAccounts[0], + ...(opts?.customClaimer + ? { + customClaimer: opts.customClaimer, + instantShare: 0, + } + : {}), + }, + stableCoins.dai, + 100, + { from: regularAccounts[0] }, + ); + }; + + it('should fail: approve request when recipient got blacklisted', async () => { + const fixture = await loadRvFixture(); + const { + owner, + redemptionVault, + mTBILL, + mTokenToUsdDataFeed, + blackListableTester, + accessControl, + regularAccounts, + } = fixture; + await setupPendingRedeemRequest(fixture); + + await blackList( + { blacklistable: blackListableTester, accessControl, owner }, + regularAccounts[0], + ); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('5'), + { revertCustomError: acErrors.WMAC_BLACKLISTED }, + ); + }); + + it('should fail: approve request when recipient got ungreenlisted when greenlist enable flag is true', async () => { + const fixture = await loadRvFixture(); + const { owner, redemptionVault, mTBILL, mTokenToUsdDataFeed } = + fixture; + await setupPendingRedeemRequest(fixture); + + await redemptionVault.setGreenlistEnable(true); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('5'), + { + revertCustomError: { + customErrorName: 'NotGreenlisted', + }, + }, + ); + }); + + it('should fail: approve request when recipient got sanction listed', async () => { + const fixture = await loadRvFixture(); + const { + owner, + redemptionVault, + mTBILL, + mTokenToUsdDataFeed, + regularAccounts, + mockedSanctionsList, + } = fixture; + await setupPendingRedeemRequest(fixture); + + await sanctionUser( + { sanctionsList: mockedSanctionsList }, + regularAccounts[0], + ); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('5'), + { + revertCustomError: { + customErrorName: 'Sanctioned', + }, + }, + ); + }); + }); + + it('should fail: request already processed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + +requestId, + parseUnits('1'), + ); + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + +requestId, + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('approve request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + +requestId, + parseUnits('1'), + ); + }); + + it('should succeed when other approve entrypoints are paused', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await pauseOtherRedemptionApproveFns( + redemptionVault, + encodeFnSelector('approveRequest(uint256,uint256,bool)'), + ); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + +requestId, + parseUnits('1'), + ); + }); + + it('should approve request in non sequential order when sequentialRequestProcessing is disabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 1, + parseUnits('1'), + ); + }); + + it('should fail: approve request in non sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 300); + await approveBase18(owner, mTBILL, redemptionVault, 300); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async (i) => { + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('1'), + ); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 2, + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'InvalidRequestSequence', + args: [2, 1], + }, + }, + ); + }); + + it('should approve request id 0 and then id 1 when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + parseUnits('1'), + ); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 1, + parseUnits('1'), + ); + }); + + it('should enforce fifo across separate transactions when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVault, 1000); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await asyncForEach( + Array.from({ length: 9 }, (_, i) => i), + async (i) => { + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await asyncForEach( + [0, 1, 2], + async (requestId) => { + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('1'), + ); + }, + true, + ); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 3, + parseUnits('1'), + ); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 5, + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'InvalidRequestSequence', + args: [5, 4], + }, + }, + ); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 4, + parseUnits('1'), + ); + + await asyncForEach( + [6, 7, 8], + async (requestId) => { + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'InvalidRequestSequence', + args: [requestId, 5], + }, + }, + ); + }, + true, + ); + + await asyncForEach( + [5, 6, 7], + async (requestId) => { + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + requestId, + parseUnits('1'), + ); + }, + true, + ); + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 8, + parseUnits('1'), + ); + }); + }); + + describe('avgRate=true', async () => { + it('should fail: call from address without vault admin role', async () => { + const { + redemptionVault, + regularAccounts, + mTokenToUsdDataFeed, + mTBILL, + } = await loadRvFixture(); + await approveRedeemRequestTest( + { + redemptionVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + 1, + parseUnits('1'), + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: when function is paused', async () => { + const { owner, redemptionVault, mTBILL, mTokenToUsdDataFeed } = + await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('approveRequest(uint256,uint256,bool)'), + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + 0, + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('when instant part is 0', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + 0, + parseUnits('5'), + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + 1, + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should fail: request already processed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + +requestId, + parseUnits('5'), + ); + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + +requestId, + parseUnits('5'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('when calclulated holdback part rate is 0 should use the price passed as newMTokenRate', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + +requestId, + parseUnits('1'), + ); + }); + + it('should not check for deviation tolerance', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + await setVariabilityToleranceTest( + { vault: redemptionVault, owner }, + 1, + ); + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + +requestId, + parseUnits('4'), + ); + }); + + it('approve request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + +requestId, + parseUnits('5'), + ); + }); + + it('should succeed when other approve entrypoints are paused', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await pauseOtherRedemptionApproveFns( + redemptionVault, + encodeFnSelector('approveRequest(uint256,uint256,bool)'), + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + +requestId, + parseUnits('5'), + ); + }); + + it('should approve request in non sequential order when sequentialRequestProcessing is disabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + 1, + parseUnits('5'), + ); + }); + + it('should fail: approve request in non sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 300); + await approveBase18(owner, mTBILL, redemptionVault, 300); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async (i) => { + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + 0, + parseUnits('5'), + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + 2, + parseUnits('5'), + { + revertCustomError: { + customErrorName: 'InvalidRequestSequence', + args: [2, 1], + }, + }, + ); + }); + + it('should approve request id 0 and then id 1 when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + 0, + parseUnits('5'), + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + 1, + parseUnits('5'), + ); + }); + }); + }); + + describe('safeBulkApproveRequestAtSavedRate()', async () => { + it('should fail: call from address without vault admin role', async () => { + const { + redemptionVault, + regularAccounts, + mTokenToUsdDataFeed, + mTBILL, + } = await loadRvFixture(); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + }, + [{ id: 1 }], + 'request-rate', + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }], + 'request-rate', + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should skip requests above maxApproveRequestId without reverting', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await setMaxApproveRequestIdTest({ redemptionVault, owner }, 0); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + 'request-rate', + ); + }); + + it('should fail: request already processed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + 'request-rate', + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + 'request-rate', + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: when function is paused', async () => { + const { owner, redemptionVault, mTBILL, mTokenToUsdDataFeed } = + await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('safeBulkApproveRequestAtSavedRate(uint256[])'), + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }], + 'request-rate', + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: process multiple requests, when one of them already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 0, + parseUnits('5.000001'), + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + 'request-rate', + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when couple of them have equal id', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 0, + parseUnits('5.000001'), + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 1 }], + 'request-rate', + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('approve 1 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + 'request-rate', + ); + }); + + it('approve 2 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + 'request-rate', + ); + }); + + it('approve 10 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + regularAccounts, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVault, 1000); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await asyncForEach( + Array.from({ length: 10 }, (_, i) => i), + async (i) => { + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient: regularAccounts[i], + }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + Array.from({ length: 10 }, (_, i) => ({ id: i })), + 'request-rate', + ); + }); + + it('approve 1 request when there is not enough liquidity', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId, expectedToExecute: false }], + 'request-rate', + ); + }); + + it('should fail: when token fee is not 0, user is not fee waived and request redeemer do not have enough tokens to transfer the fee', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + regularAccounts, + } = await loadRvFixture(); + + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 100, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 0, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0, expectedToExecute: false }], + 'request-rate', + ); + }); + + it('approve 2 request when there is enough liquidity only for first one', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 600); + + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 600, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [ + { id: 0, expectedToExecute: true }, + { id: 1, expectedToExecute: false }, + ], + 'request-rate', + ); + }); + + it('approve 2 requests both with different payment tokens', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.usdc, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await approveBase18( + requestRedeemer, + stableCoins.usdc, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.usdc, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + 'request-rate', + ); + }); + + it('approve 2 requests both with different payment tokens when there is not enough liquidity for first one', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.usdc, requestRedeemer, 100000); + + await approveBase18( + requestRedeemer, + stableCoins.usdc, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.usdc, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0, expectedToExecute: false }], + 'request-rate', + ); + }); + + it('should succeed when other approve entrypoints are paused', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await pauseOtherRedemptionApproveFns( + redemptionVault, + encodeFnSelector('safeBulkApproveRequestAtSavedRate(uint256[])'), + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + 'request-rate', + ); + }); + + it('should approve requests in non sequential order when sequentialRequestProcessing is disabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + 'request-rate', + ); + }); + + it('should skip out-of-order bulk approvals without reverting when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [ + { id: 1, expectedToExecute: false }, + { id: 0, expectedToExecute: true }, + ], + 'request-rate', + ); + }); + + it('should approve requests in sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 300); + await approveBase18(owner, mTBILL, redemptionVault, 300); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async () => { + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1 }, { id: 2 }], + 'request-rate', + ); + }); + + it('should not approve requests after insufficient liquidity when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 600); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 600, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [ + { id: 0, expectedToExecute: true }, + { id: 1, expectedToExecute: false }, + ], + 'request-rate', + ); + }); + }); + + describe('safeBulkApproveRequest() (custom price overload)', async () => { + it('should fail: call from address without vault admin role', async () => { + const { + redemptionVault, + regularAccounts, + mTokenToUsdDataFeed, + mTBILL, + } = await loadRvFixture(); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + }, + [{ id: 1 }], + parseUnits('1'), + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: when function is paused', async () => { + const { owner, redemptionVault, mTBILL, mTokenToUsdDataFeed } = + await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('safeBulkApproveRequest(uint256[],uint256)'), + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }], + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }], + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should skip requests above maxApproveRequestId without reverting', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await setMaxApproveRequestIdTest({ redemptionVault, owner }, 0); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + parseUnits('5'), + ); + }); + + it('should fail: if new rate greater then variabilityTolerance', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + parseUnits('6'), + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: if new rate lower then variabilityTolerance', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + parseUnits('4'), + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: request already processed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + parseUnits('5.000001'), + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + parseUnits('5.00001'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when one of them already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 0, + parseUnits('5.000001'), + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + parseUnits('5.00001'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when couple of them have equal id', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 0, + parseUnits('5.000001'), + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 1 }], + parseUnits('5.00001'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('approve 1 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + parseUnits('5.000001'), + ); + }); + + it('approve 2 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + parseUnits('5.000001'), + ); + }); + + it('approve 10 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + regularAccounts, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVault, 1000); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await asyncForEach( + Array.from({ length: 10 }, (_, i) => i), + async (i) => { + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient: regularAccounts[i], + }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + Array.from({ length: 10 }, (_, i) => ({ id: i })), + parseUnits('5.000001'), + ); + }); + + it('approve 1 request when there is not enough liquidity', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId, expectedToExecute: false }], + parseUnits('5.000001'), + ); + }); + + it('approve 2 request when there is enough liquidity only for first one', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 600); + + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 600, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [ + { id: 0, expectedToExecute: true }, + { id: 1, expectedToExecute: false }, + ], + parseUnits('5.000001'), + ); + }); + + it('approve 2 requests both with different payment tokens', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.usdc, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await approveBase18( + requestRedeemer, + stableCoins.usdc, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.usdc, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + parseUnits('5.000001'), + ); + }); + + it('approve 2 requests both with different payment tokens when there is not enough liquidity for first one', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.usdc, requestRedeemer, 100000); + + await approveBase18( + requestRedeemer, + stableCoins.usdc, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.usdc, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0, expectedToExecute: false }], + parseUnits('5.000001'), + ); + }); + + it('should succeed when other approve entrypoints are paused', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await pauseOtherRedemptionApproveFns( + redemptionVault, + encodeFnSelector('safeBulkApproveRequest(uint256[],uint256)'), + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + parseUnits('5.000001'), + ); + }); + + it('should approve requests in non sequential order when sequentialRequestProcessing is disabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + parseUnits('5.000001'), + ); + }); + + it('should skip out-of-order bulk approvals without reverting when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [ + { id: 1, expectedToExecute: false }, + { id: 0, expectedToExecute: true }, + ], + parseUnits('5.000001'), + ); + }); + + it('should approve requests in sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 300); + await approveBase18(owner, mTBILL, redemptionVault, 300); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async (i) => { + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1 }, { id: 2 }], + parseUnits('5.000001'), + ); + }); + + it('should not approve requests after insufficient liquidity when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 600); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 600, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [ + { id: 0, expectedToExecute: true }, + { id: 1, expectedToExecute: false }, + ], + parseUnits('5.000001'), + ); + }); + }); + + describe('safeBulkApproveRequest() (current price overload)', async () => { + it('should fail: call from address without vault admin role', async () => { + const { + redemptionVault, + regularAccounts, + mTokenToUsdDataFeed, + mTBILL, + } = await loadRvFixture(); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + }, + [{ id: 1 }], + undefined, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: when function is paused', async () => { + const { owner, redemptionVault, mTBILL, mTokenToUsdDataFeed } = + await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('safeBulkApproveRequest(uint256[])'), + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }], + undefined, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }], + undefined, + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should skip requests above maxApproveRequestId without reverting', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await setMaxApproveRequestIdTest({ redemptionVault, owner }, 0); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + undefined, + ); + }); + + it('should fail: if new rate greater then variabilityTolerance', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 6); + + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + undefined, + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: if new rate lower then variabilityTolerance', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 4); + + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + undefined, + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: request already processed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + undefined, + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + undefined, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when one of them already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 0, + parseUnits('5.000001'), + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + undefined, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when couple of them have equal id', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + 0, + parseUnits('5.000001'), + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 1 }], + undefined, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('approve 1 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + undefined, + ); + }); + + it('approve 1 request from vaut admin account when 10% growth is applied', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + customFeedGrowth, + } = await loadRvFixture(); + + await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); + await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, 10); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + undefined, + ); + }); + + it('approve 1 request from vaut admin account when -10% growth is applied', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + customFeedGrowth, + } = await loadRvFixture(); + + await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); + await setMinGrowthApr({ owner, customFeedGrowth }, -10); + await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, -10); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + undefined, + ); + }); + + it('approve 2 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + undefined, + ); + }); + + it('approve 10 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + regularAccounts, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVault, 1000); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await asyncForEach( + Array.from({ length: 10 }, (_, i) => i), + async (i) => { + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient: regularAccounts[i], + }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + Array.from({ length: 10 }, (_, i) => ({ id: i })), + undefined, + ); + }); + + it('approve 1 request when there is not enough liquidity', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId, expectedToExecute: false }], + undefined, + ); + }); + + it('approve 2 request when there is enough liquidity only for first one', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 600); + + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 600, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [ + { id: 0, expectedToExecute: true }, + { id: 1, expectedToExecute: false }, + ], + undefined, + ); + }); + + it('approve 2 requests both with different payment tokens', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.usdc, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await approveBase18( + requestRedeemer, + stableCoins.usdc, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.usdc, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + undefined, + ); + }); + + it('approve 2 requests both with different payment tokens when there is not enough liquidity for first one', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.usdc, requestRedeemer, 100000); + + await approveBase18( + requestRedeemer, + stableCoins.usdc, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.usdc, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0, expectedToExecute: false }], + undefined, + ); + }); + + it('should succeed when other approve entrypoints are paused', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await pauseOtherRedemptionApproveFns( + redemptionVault, + encodeFnSelector('safeBulkApproveRequest(uint256[])'), + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + undefined, + ); + }); + + it('should approve requests in non sequential order when sequentialRequestProcessing is disabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 1 }, { id: 0 }], + undefined, + ); + }); + + it('should skip out-of-order bulk approvals without reverting when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [ + { id: 1, expectedToExecute: false }, + { id: 0, expectedToExecute: true }, + ], + undefined, + ); + }); + + it('should approve requests in sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 300); + await approveBase18(owner, mTBILL, redemptionVault, 300); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async (i) => { + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: 0 }, { id: 1 }, { id: 2 }], + undefined, + ); + }); + + it('should not approve requests after insufficient liquidity when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 600); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 600, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [ + { id: 0, expectedToExecute: true }, + { id: 1, expectedToExecute: false }, + ], + undefined, + ); + }); + }); + + describe('safeBulkApproveRequestAvgRate() (custom price overload)', async () => { + it('should fail: call from address without vault admin role', async () => { + const { + redemptionVault, + regularAccounts, + mTokenToUsdDataFeed, + mTBILL, + } = await loadRvFixture(); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }], + parseUnits('1'), + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: when function is paused', async () => { + const { owner, redemptionVault, mTBILL, mTokenToUsdDataFeed } = + await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector( + 'safeBulkApproveRequestAvgRate(uint256[],uint256)', + ), + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }], + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }], + parseUnits('1'), + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should skip requests above maxApproveRequestId without reverting', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await setMaxApproveRequestIdTest({ redemptionVault, owner }, 0); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + parseUnits('5'), + ); + }); + + it('should fail: if new rate greater then variabilityTolerance', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + parseUnits('6'), + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: if new rate lower then variabilityTolerance', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + parseUnits('4'), + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: request already processed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + [{ id: requestId }], + parseUnits('5.000001'), + ); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + parseUnits('5.00001'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when one of them already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + 0, + parseUnits('5.000001'), + ); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + parseUnits('5.00001'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when couple of them have equal id', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + 0, + parseUnits('5.000001'), + ); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 1 }], + parseUnits('5.00001'), + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('process multiple requests, when one of them does not have instant part', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + parseUnits('5.00001'), + ); + }); + + it('approve 1 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + parseUnits('5.000001'), + ); + }); + + it('approve 2 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + parseUnits('5.000001'), + ); + }); + + it('approve 10 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + regularAccounts, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVault, 1000); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await asyncForEach( + Array.from({ length: 10 }, (_, i) => i), + async (i) => { + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient: regularAccounts[i], + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + Array.from({ length: 10 }, (_, i) => ({ id: i })), + parseUnits('5.000001'), + ); + }); + + it('approve 1 request when there is not enough liquidity', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId, expectedToExecute: false }], + parseUnits('5.000001'), + ); + }); + + it('approve 2 request when there is enough liquidity only for first one', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 300); + await mintToken(stableCoins.dai, redemptionVault, 600); + + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 600, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [ + { id: 0, expectedToExecute: true }, + { id: 1, expectedToExecute: false }, + ], + parseUnits('5.000001'), + ); + }); + + it('approve 2 requests both with different payment tokens', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.usdc, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(stableCoins.usdc, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await approveBase18( + requestRedeemer, + stableCoins.usdc, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.usdc, + 100, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + parseUnits('5.000001'), + ); + }); + + it('approve 2 requests both with different payment tokens when there is not enough liquidity for first one', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.usdc, requestRedeemer, 100000); + await mintToken(stableCoins.usdc, redemptionVault, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + + await approveBase18( + requestRedeemer, + stableCoins.usdc, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.usdc, + 100, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0, expectedToExecute: false }], + parseUnits('5.000001'), + ); + }); + + it('should succeed when other approve entrypoints are paused', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await pauseOtherRedemptionApproveFns( + redemptionVault, + encodeFnSelector( + 'safeBulkApproveRequestAvgRate(uint256[],uint256)', + ), + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + parseUnits('5.000001'), + ); + }); + + it('should approve requests in non sequential order when sequentialRequestProcessing is disabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + parseUnits('5'), + ); + }); + + it('should skip out-of-order bulk approvals without reverting when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [ + { id: 1, expectedToExecute: false }, + { id: 0, expectedToExecute: true }, + ], + parseUnits('5'), + ); + }); + + it('should approve requests in sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 300); + await approveBase18(owner, mTBILL, redemptionVault, 300); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async (i) => { + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }, { id: 1 }, { id: 2 }], + parseUnits('5'), + ); + }); + + it('should not approve requests after insufficient liquidity when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 300); + await mintToken(stableCoins.dai, redemptionVault, 600); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 600, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [ + { id: 0, expectedToExecute: true }, + { id: 1, expectedToExecute: false }, + ], + parseUnits('5'), + ); + }); + }); + + describe('safeBulkApproveRequestAvgRate() (current price overload)', async () => { + it('should fail: call from address without vault admin role', async () => { + const { + redemptionVault, + regularAccounts, + mTokenToUsdDataFeed, + mTBILL, + } = await loadRvFixture(); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }], + undefined, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: when function is paused', async () => { + const { owner, redemptionVault, mTBILL, mTokenToUsdDataFeed } = + await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('safeBulkApproveRequestAvgRate(uint256[])'), + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }], + undefined, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }], + undefined, + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should skip requests above maxApproveRequestId without reverting', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await setMaxApproveRequestIdTest({ redemptionVault, owner }, 0); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }, { id: 1, expectedToExecute: false }], + undefined, + ); + }); + + it('should fail: if new rate greater then variabilityTolerance', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 6); + + const requestId = 0; + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: if new rate lower then variabilityTolerance', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 4); + + const requestId = 0; + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + { + revertCustomError: { + customErrorName: 'PriceVariationExceeded', + }, + }, + ); + }); + + it('should fail: request already processed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + ); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when one of them already precessed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + 0, + parseUnits('5.000001'), + ); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + undefined, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: process multiple requests, when couple of them have equal id', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + 0, + parseUnits('5.000001'), + ); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 1 }], + undefined, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('when one of the requests instant part is 0', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + undefined, + ); + }); + + it('approve 1 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + ); + }); + + it('approve 1 request from vaut admin account when 10% growth is applied', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + customFeedGrowth, + } = await loadRvFixture(); + + await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); + await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, 10); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + ); + }); + + it('approve 1 request from vaut admin account when -10% growth is applied', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + customFeedGrowth, + } = await loadRvFixture(); + + await mTokenToUsdDataFeed.changeAggregator(customFeedGrowth.address); + await setMinGrowthApr({ owner, customFeedGrowth }, -10); + await setRoundDataGrowth({ owner, customFeedGrowth }, 1, -1000, -10); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + ); + }); + + it('approve 2 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + undefined, + ); + }); + + it('approve 10 request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + regularAccounts, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 1000); + await approveBase18(owner, mTBILL, redemptionVault, 1000); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await asyncForEach( + Array.from({ length: 10 }, (_, i) => i), + async (i) => { + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + customRecipient: regularAccounts[i], + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + Array.from({ length: 10 }, (_, i) => ({ id: i })), + undefined, + ); + }); + + it('approve 1 request when there is not enough liquidity', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId, expectedToExecute: false }], + undefined, + ); + }); + + it('approve 2 request when there is enough liquidity only for first one', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 300); + await mintToken(stableCoins.dai, redemptionVault, 1000); + + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 600, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [ + { id: 0, expectedToExecute: true }, + { id: 1, expectedToExecute: false }, + ], + undefined, + ); + }); + + it('approve 2 requests both with different payment tokens', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.usdc, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(stableCoins.usdc, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await approveBase18( + requestRedeemer, + stableCoins.usdc, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.usdc, + 100, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + undefined, + ); + }); + + it('approve 2 requests both with different payment tokens when there is not enough liquidity for first one', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.usdc, requestRedeemer, 100000); + await mintToken(stableCoins.usdc, redemptionVault, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.usdc, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.usdc, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.usdc, + 100, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0, expectedToExecute: false }], + undefined, + ); + }); + + it('should succeed when other approve entrypoints are paused', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await pauseOtherRedemptionApproveFns( + redemptionVault, + encodeFnSelector('safeBulkApproveRequestAvgRate(uint256[])'), + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: requestId }], + undefined, + ); + }); + + it('should approve requests in non sequential order when sequentialRequestProcessing is disabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 1 }, { id: 0 }], + undefined, + ); + }); + + it('should skip out-of-order bulk approvals without reverting when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [ + { id: 1, expectedToExecute: false }, + { id: 0, expectedToExecute: true }, + ], + undefined, + ); + }); + + it('should approve requests in sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await mintToken(stableCoins.dai, redemptionVault, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await mintToken(mTBILL, owner, 300); + await approveBase18(owner, mTBILL, redemptionVault, 300); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async (i) => { + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [{ id: 0 }, { id: 1 }, { id: 2 }], + undefined, + ); + }); + + it('should not approve requests after insufficient liquidity when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + requestRedeemer, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, requestRedeemer, 300); + await mintToken(stableCoins.dai, redemptionVault, 600); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 600, + ); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + instantShare: 50_00, + }, + stableCoins.dai, + 100, + ); + await safeBulkApproveRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + isAvgRate: true, + }, + [ + { id: 0, expectedToExecute: true }, + { id: 1, expectedToExecute: false }, + ], + undefined, + ); + }); + }); + + describe('rejectRequest()', async () => { + it('should fail: call from address without vault admin role', async () => { + const { + redemptionVault, + regularAccounts, + mTokenToUsdDataFeed, + mTBILL, + } = await loadRvFixture(); + await rejectRedeemRequestTest( + { + redemptionVault, + owner: regularAccounts[1], + mTBILL, + mTokenToUsdDataFeed, + }, + 1, + { + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('should fail: when function is paused', async () => { + const { owner, redemptionVault, mTokenToUsdDataFeed, mTBILL } = + await loadRvFixture(); + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('rejectRequest(uint256)'), + ); + + await rejectRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should fail: request by id not exist', async () => { + const { + owner, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await rejectRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 1, + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should fail: request already processed', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.001); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await rejectRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + +requestId, + ); + await rejectRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + +requestId, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('safe approve request from vaut admin account', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + mTokenToUsdDataFeed, + dataFeed, + } = await loadRvFixture(); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, mTBILL, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + const requestId = 0; + + await rejectRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + +requestId, + ); + }); + + it('should reject request id 0 and then id 1 when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, owner, 200); + await approveBase18(owner, mTBILL, redemptionVault, 200); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + + await rejectRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 0, + ); + + await rejectRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 1, + ); + }); + + it('should fail: reject request id in non sequential order when sequentialRequestProcessing is enabled', async () => { + const { + owner, + mockedAggregator, + mockedAggregatorMToken, + redemptionVault, + stableCoins, + mTBILL, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await setSequentialRequestProcessingTest( + { vault: redemptionVault, owner }, + true, + ); + + await mintToken(stableCoins.dai, redemptionVault, 100000); + await mintToken(mTBILL, owner, 300); + await approveBase18(owner, mTBILL, redemptionVault, 300); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1.03); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 5); + + await asyncForEach( + Array.from({ length: 3 }, (_, i) => i), + async (i) => { + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + ); + }, + true, + ); + + await rejectRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + 2, + { + revertCustomError: { + customErrorName: 'InvalidRequestSequence', + args: [2, 0], + }, + }, + ); + }); + }); + + describe('redeemRequest() complex', () => { + it('should fail: when is paused', async () => { + const { + redemptionVault, + owner, + mTBILL, + stableCoins, + regularAccounts, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await pauseVault({ pauseManager, owner }, redemptionVault); + await mintToken(stableCoins.dai, redemptionVault, 100); + await mintToken(mTBILL, regularAccounts[0], 100); + await approveBase18( + regularAccounts[0], + stableCoins.dai, + redemptionVault, + 100, + ); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + from: regularAccounts[0], + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('is on pause, but admin can use everything', async () => { + const { + redemptionVault, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + + await pauseVault({ pauseManager, owner }, redemptionVault); + + await mintToken(stableCoins.dai, redemptionVault, 1000); + await mintToken(mTBILL, owner, 100); + await approveBase18(owner, stableCoins.dai, redemptionVault, 100); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('call for amount == minAmount, then approve', async () => { + const { + redemptionVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await mintToken(mTBILL, owner, 100_000); + await mintToken(stableCoins.dai, requestRedeemer, 100000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 100000, + ); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100_000, + ); + + const requestId = 0; + + await approveRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + +requestId, + parseUnits('1'), + ); + }); + + it('call for amount == minAmount, then safe approve', async () => { + const { + redemptionVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + requestRedeemer, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await mintToken(mTBILL, owner, 100_000); + await mintToken(stableCoins.dai, requestRedeemer, 1000000); + await approveBase18( + requestRedeemer, + stableCoins.dai, + redemptionVault, + 1000000, + ); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100_000, + ); + + const requestId = 0; + + await approveRedeemRequestTest( + { + redemptionVault, + owner, + mTBILL, + mTokenToUsdDataFeed, + }, + +requestId, + parseUnits('1.000001'), + ); + }); + + it('call for amount == minAmount, then reject', async () => { + const { + redemptionVault, + mockedAggregator, + mockedAggregatorMToken, + owner, + mTBILL, + stableCoins, + dataFeed, + mTokenToUsdDataFeed, + } = await loadRvFixture(); + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator }, 1); + + await mintToken(mTBILL, owner, 100_000); + await mintToken(stableCoins.dai, redemptionVault, 100_000); + await approveBase18(owner, mTBILL, redemptionVault, 100_000); + + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setMinAmountTest({ vault: redemptionVault, owner }, 100_000); + + await redeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoins.dai, + 100_000, + ); + + const requestId = 0; + + await rejectRedeemRequestTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + +requestId, + ); + }); + }); + + describe('loan operations', () => { + const prepareTest = async ( + fixture: DefaultFixture, + stableCoin: ERC20Mock, + setupVault = true, + ) => { + const { + redemptionVault, + owner, + mTBILL, + mTokenLoan, + loanLp, + redemptionVaultLoanSwapper, + dataFeed, + mTokenToUsdDataFeed, + mockedAggregator, + mockedAggregatorMToken, + } = fixture; + + await mintToken(mTBILL, owner, 100); + await mintToken(mTokenLoan, loanLp, 1000); + await approveBase18(loanLp, mTokenLoan, redemptionVault, 1000); + await mintToken(stableCoin, redemptionVaultLoanSwapper, 1000); + + await approveBase18(loanLp, stableCoin, redemptionVault, 1000); + + if (setupVault) { + await withdrawTest( + { vault: redemptionVault, owner }, + stableCoin, + await stableCoin.balanceOf(redemptionVault.address), + ); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoin, + dataFeed.address, + 0, + true, + ); + + await addPaymentTokenTest( + { vault: redemptionVaultLoanSwapper, owner }, + stableCoin, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + await setRoundData({ mockedAggregator }, 1); + } + + await redeemInstantTest( + { redemptionVault, owner, mTBILL, mTokenToUsdDataFeed }, + stableCoin, + 100, + ); + }; + describe('bulkRepayLpLoanRequest()', () => { + it('should fail: when function is paused', async () => { + const fixture = await loadRvFixture(); + const { redemptionVault, owner, mTBILL } = fixture; + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('bulkRepayLpLoanRequest(uint256[])'), + ); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('approve 1 request', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await prepareTest(fixture, stableCoins.dai); + + await mintToken(stableCoins.dai, loanRepaymentAddress, 100); + + await approveBase18( + loanRepaymentAddress, + stableCoins.dai, + redemptionVault, + 100, + ); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + ); + }); + + it('approve 2 request with same token out', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await prepareTest(fixture, stableCoins.dai); + await prepareTest(fixture, stableCoins.dai, false); + + await mintToken(stableCoins.dai, loanRepaymentAddress, 200); + + await approveBase18( + loanRepaymentAddress, + stableCoins.dai, + redemptionVault, + 200, + ); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }, { id: 1 }], + ); + }); + + it('approve 2 request with different token out', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await prepareTest(fixture, stableCoins.dai); + await prepareTest(fixture, stableCoins.usdc); + + await mintToken(stableCoins.dai, loanRepaymentAddress, 200); + await mintToken(stableCoins.usdc, loanRepaymentAddress, 200); + + await approveBase18( + loanRepaymentAddress, + stableCoins.dai, + redemptionVault, + 100, + ); + + await approveBase18( + loanRepaymentAddress, + stableCoins.usdc, + redemptionVault, + 100, + ); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }, { id: 1 }], + ); + }); + + it('should fail: when loan repayment address does not have enough balance', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await prepareTest(fixture, stableCoins.dai); + + await approveBase18( + loanRepaymentAddress, + stableCoins.dai, + redemptionVault, + 100, + ); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('should fail: when loan repayment address does not have enough allowance', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await prepareTest(fixture, stableCoins.dai); + + await mintToken(stableCoins.dai, loanRepaymentAddress, 100); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + { + revertMessage: 'ERC20: insufficient allowance', + }, + ); + }); + + it('should fail: when loan repayment address have balance for lp transfer but not enough for fee transfer', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await setInstantFeeTest({ vault: redemptionVault, owner }, 100); + await prepareTest(fixture, stableCoins.dai); + + await mintToken(stableCoins.dai, loanRepaymentAddress, 99); + await approveBase18( + loanRepaymentAddress, + stableCoins.dai, + redemptionVault, + 100, + ); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + { + revertMessage: 'ERC20: transfer amount exceeds balance', + }, + ); + }); + + it('should fail: request was already approved', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await prepareTest(fixture, stableCoins.dai); + + await mintToken(stableCoins.dai, loanRepaymentAddress, 100); + await approveBase18( + loanRepaymentAddress, + stableCoins.dai, + redemptionVault, + 100, + ); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + ); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: request not exist', async () => { + const fixture = await loadRvFixture(); + const { redemptionVault, owner, mTBILL } = fixture; + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should fail: call from not vault admin', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + regularAccounts, + stableCoins, + loanRepaymentAddress, + } = fixture; + + await prepareTest(fixture, stableCoins.dai); + + await mintToken(stableCoins.dai, loanRepaymentAddress, 100); + await approveBase18( + loanRepaymentAddress, + stableCoins.dai, + redemptionVault, + 100, + ); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + + it('approve 1 request when loanApr is not zero but does not exceed instant fee', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await setInstantFeeTest({ vault: redemptionVault, owner }, 100); + await prepareTest(fixture, stableCoins.dai); + await increase(days(365)); + + await mintToken(stableCoins.dai, loanRepaymentAddress, 101); + await approveBase18( + loanRepaymentAddress, + stableCoins.dai, + redemptionVault, + 101, + ); + + await setLoanAprTest({ redemptionVault, owner }, 50); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + ); + }); + + it('approve 1 request when loanApr is not zero and exceeds instant fee', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await setInstantFeeTest({ vault: redemptionVault, owner }, 0); + await prepareTest(fixture, stableCoins.usdt); + const request = await redemptionVault.loanRequests(0); + await ethers.provider.send('evm_setNextBlockTimestamp', [ + request.createdAt.toNumber() + days(365), + ]); + + await mintToken(stableCoins.usdt, loanRepaymentAddress, 1000); + await approveBase18( + loanRepaymentAddress, + stableCoins.usdt, + redemptionVault, + 1000, + ); + + await setLoanAprTest({ redemptionVault, owner }, 10000); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + ); + }); + + it('approve 1 request when instant fee changed after request creation', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await setInstantFeeTest({ vault: redemptionVault, owner }, 200); + await prepareTest(fixture, stableCoins.dai); + await setInstantFeeTest({ vault: redemptionVault, owner }, 50); + await increase(days(365)); + + await mintToken(stableCoins.dai, loanRepaymentAddress, 102); + await approveBase18( + loanRepaymentAddress, + stableCoins.dai, + redemptionVault, + 102, + ); + + await setLoanAprTest({ redemptionVault, owner }, 100); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + ); + }); + + it('approve 1 request when total fee exceeds actual repayment amount', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await setInstantFeeTest({ vault: redemptionVault, owner }, 0); + await prepareTest(fixture, stableCoins.usdt); + const request = await redemptionVault.loanRequests(0); + await ethers.provider.send('evm_setNextBlockTimestamp', [ + request.createdAt.toNumber() + days(365), + ]); + + await mintToken(stableCoins.usdt, loanRepaymentAddress, 1000); + await approveBase18( + loanRepaymentAddress, + stableCoins.usdt, + redemptionVault, + 1000, + ); + + await setLoanAprTest({ redemptionVault, owner }, 20000); + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + ); + }); + + it('approve 2 request with same token out when loanApr is not zero', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await prepareTest(fixture, stableCoins.usdt); + await prepareTest(fixture, stableCoins.usdt, false); + await setInstantFeeTest({ vault: redemptionVault, owner }, 0); + + const r0 = await redemptionVault.loanRequests(0); + const r1 = await redemptionVault.loanRequests(1); + await ethers.provider.send('evm_setNextBlockTimestamp', [ + Math.max(r0.createdAt.toNumber(), r1.createdAt.toNumber()) + + days(365), + ]); + + await mintToken(stableCoins.usdt, loanRepaymentAddress, 2000); + await approveBase18( + loanRepaymentAddress, + stableCoins.usdt, + redemptionVault, + 2000, + ); + + await setLoanAprTest({ redemptionVault, owner }, 10000); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }, { id: 1 }], + ); + }); + + it('approve 2 request with different token out when loanApr is not zero', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await prepareTest(fixture, stableCoins.dai); + await prepareTest(fixture, stableCoins.usdc); + await setInstantFeeTest({ vault: redemptionVault, owner }, 0); + + const r0 = await redemptionVault.loanRequests(0); + const r1 = await redemptionVault.loanRequests(1); + await ethers.provider.send('evm_setNextBlockTimestamp', [ + Math.max(r0.createdAt.toNumber(), r1.createdAt.toNumber()) + + days(1), + ]); + + await mintToken(stableCoins.dai, loanRepaymentAddress, 1000); + await mintToken(stableCoins.usdc, loanRepaymentAddress, 1000); + await approveBase18( + loanRepaymentAddress, + stableCoins.dai, + redemptionVault, + 1000, + ); + await approveBase18( + loanRepaymentAddress, + stableCoins.usdc, + redemptionVault, + 1000, + ); + + await setLoanAprTest({ redemptionVault, owner }, 5000); + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }, { id: 1 }], + ); + }); + + it('approve 3 request with same token out when loanApr is not zero', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await prepareTest(fixture, stableCoins.usdt); + await prepareTest(fixture, stableCoins.usdt, false); + await prepareTest(fixture, stableCoins.usdt, false); + await setInstantFeeTest({ vault: redemptionVault, owner }, 0); + + const r0 = await redemptionVault.loanRequests(0); + const r1 = await redemptionVault.loanRequests(1); + const r2 = await redemptionVault.loanRequests(2); + await ethers.provider.send('evm_setNextBlockTimestamp', [ + Math.max( + r0.createdAt.toNumber(), + r1.createdAt.toNumber(), + r2.createdAt.toNumber(), + ) + days(365), + ]); + + await mintToken(stableCoins.usdt, loanRepaymentAddress, 5000); + await approveBase18( + loanRepaymentAddress, + stableCoins.usdt, + redemptionVault, + 5000, + ); + + await setLoanAprTest({ redemptionVault, owner }, 5000); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }, { id: 1 }, { id: 2 }], + ); + }); + + it('approve 1 request when loanApr accrued interest is truncated to token decimals', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await setInstantFeeTest({ vault: redemptionVault, owner }, 0); + await prepareTest(fixture, stableCoins.usdc6); + await increase(days(1)); + + const request = await redemptionVault.loanRequests(0); + const tokenDecimals = await stableCoins.usdc6.decimals(); + const loanApr = BigNumber.from(3333); + const currentTimestamp = await getCurrentBlockTimestamp(); + const duration = BigNumber.from(currentTimestamp).sub( + request.createdAt, + ); + + const accruedInterestRaw = request.amountTokenOut + .mul(loanApr) + .mul(duration) + .div(BigNumber.from(10_000).mul(365).mul(86400)); + + const accruedInterestTruncated = truncateToTokenDecimals( + accruedInterestRaw, + tokenDecimals, + ); + + expect(accruedInterestRaw).gt(accruedInterestTruncated); + expect(accruedInterestTruncated).gt(request.amountFee); + + await mintToken(stableCoins.usdc6, loanRepaymentAddress, 1000); + await approveBase18( + loanRepaymentAddress, + stableCoins.usdc6, + redemptionVault, + 1000, + ); + + await setLoanAprTest({ redemptionVault, owner }, loanApr); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + ); + }); + + it('approve 1 request when untruncated accrued interest exceeds instant fee but truncated does not', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + loanRepaymentAddress, + stableCoins, + } = fixture; + + await setInstantFeeTest({ vault: redemptionVault, owner }, 100); + await prepareTest(fixture, stableCoins.usdc6); + + const request = await redemptionVault.loanRequests(0); + const tokenDecimals = await stableCoins.usdc6.decimals(); + const loanApr = BigNumber.from(133); + const durationSeconds = 23_950_800; + + await ethers.provider.send('evm_setNextBlockTimestamp', [ + request.createdAt.toNumber() + durationSeconds, + ]); + await ethers.provider.send('evm_mine', []); + + const duration = BigNumber.from(durationSeconds); + const accruedInterestRaw = request.amountTokenOut + .mul(loanApr) + .mul(duration) + .div(BigNumber.from(10_000).mul(365).mul(86400)); + + const accruedInterestTruncated = truncateToTokenDecimals( + accruedInterestRaw, + tokenDecimals, + ); + + expect(accruedInterestRaw).gt(request.amountFee); + expect(accruedInterestTruncated).lte(request.amountFee); + expect(accruedInterestRaw).gt(accruedInterestTruncated); + + await mintToken(stableCoins.usdc6, loanRepaymentAddress, 100); + await approveBase18( + loanRepaymentAddress, + stableCoins.usdc6, + redemptionVault, + 100, + ); + + await setLoanAprTest({ redemptionVault, owner }, loanApr); + + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + ); + + const requestAfter = await redemptionVault.loanRequests(0); + expect(requestAfter.amountFee).eq(request.amountFee); + }); + }); + + describe('cancelLpLoanRequest()', () => { + const prepareCancelTest = async ( + fixture: DefaultFixture, + stableCoin: ERC20Mock, + ) => { + const { redemptionVault, loanRepaymentAddress } = fixture; + await prepareTest(fixture, stableCoin); + await mintToken(stableCoin, loanRepaymentAddress, 100); + await approveBase18( + loanRepaymentAddress, + stableCoin, + redemptionVault, + 100, + ); + }; + + it('should fail: when function is paused', async () => { + const fixture = await loadRvFixture(); + const { redemptionVault, owner, mTBILL } = fixture; + + await pauseVaultFn( + { pauseManager, owner }, + redemptionVault, + encodeFnSelector('cancelLpLoanRequest(uint256)'), + ); + + await cancelLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + 0, + { + revertCustomError: { + customErrorName: 'Paused', + }, + }, + ); + }); + + it('should cancel request', async () => { + const fixture = await loadRvFixture(); + const { redemptionVault, owner, mTBILL, stableCoins } = fixture; + + await prepareCancelTest(fixture, stableCoins.dai); + + await cancelLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + 0, + ); + }); + + it('should fail: request not exist', async () => { + const fixture = await loadRvFixture(); + const { redemptionVault, owner, mTBILL } = fixture; + + await cancelLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + 0, + { + revertCustomError: { + customErrorName: 'RequestNotExists', + }, + }, + ); + }); + + it('should fail: request already cancelled', async () => { + const fixture = await loadRvFixture(); + const { redemptionVault, owner, mTBILL, stableCoins } = fixture; + + await prepareCancelTest(fixture, stableCoins.dai); + + await cancelLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + 0, + ); + + await cancelLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + 0, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: request was processed', async () => { + const fixture = await loadRvFixture(); + const { redemptionVault, owner, mTBILL, stableCoins } = fixture; + + await prepareCancelTest(fixture, stableCoins.dai); + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + ); + + await cancelLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + 0, + { + revertCustomError: { + customErrorName: 'UnexpectedRequestStatus', + }, + }, + ); + }); + + it('should fail: call from not vault admin', async () => { + const fixture = await loadRvFixture(); + const { + redemptionVault, + owner, + mTBILL, + stableCoins, + regularAccounts, + } = fixture; + + await prepareCancelTest(fixture, stableCoins.dai); + await bulkRepayLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + [{ id: 0 }], + ); + + await cancelLpLoanRequestTest( + { redemptionVault, owner, mTBILL }, + 0, + { + from: regularAccounts[0], + revertCustomError: acErrors.WMAC_HASNT_PERMISSION, + }, + ); + }); + }); + }); + + describe('_obtainVaultLiquidityExternal', () => { + it('should fail: when not self call', async () => { + const { redemptionVault } = await loadRvFixture(); + + await expect( + redemptionVault._obtainVaultLiquidityExternal( + constants.AddressZero, + 0, + 0, + 0, + 0, + ), + ).to.be.revertedWithCustomError(redemptionVault, 'NotSelfCall'); + }); + + it('when is self call', async () => { + const { redemptionVault } = await loadRvFixture(); + + const impersonatedRv = await ethers.getImpersonatedSigner( + redemptionVault.address, + ); + await setBalance(impersonatedRv.address, parseUnits('100')); + + await expect( + redemptionVault + .connect(impersonatedRv) + ._obtainVaultLiquidityExternal(constants.AddressZero, 0, 0, 0, 0), + ).to.not.revertedWithCustomError(redemptionVault, 'NotSelfCall'); + }); + }); + + describe('_obtainLoanLpLiquidityExternal', () => { + it('should fail: when not self call', async () => { + const { redemptionVault } = await loadRvFixture(); + + await expect( + redemptionVault._obtainLoanLpLiquidityExternal( + constants.AddressZero, + 0, + 0, + 0, + 0, + 0, + ), + ).to.be.revertedWithCustomError(redemptionVault, 'NotSelfCall'); + }); + + it('when is self call', async () => { + const { redemptionVault } = await loadRvFixture(); + + const impersonatedRv = await ethers.getImpersonatedSigner( + redemptionVault.address, + ); + await setBalance(impersonatedRv.address, parseUnits('100')); + + await expect( + redemptionVault + .connect(impersonatedRv) + ._obtainLoanLpLiquidityExternal( + constants.AddressZero, + 0, + 0, + 0, + 0, + 0, + ), + ).to.not.revertedWithCustomError(redemptionVault, 'NotSelfCall'); + }); + }); + + describe('_convertUsdToToken', () => { + it('when amountUsd == 0', async () => { + const { redemptionVault, owner, stableCoins, dataFeed } = + await loadRvFixture(); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + expect( + ( + await redemptionVault.convertUsdToTokenTest( + 0, + stableCoins.dai.address, + 0, + ) + ).amountToken, + ).eq(0); + }); + + it('should fail: when tokenRate == 0', async () => { + const { redemptionVault } = await loadRvFixture(); + + await redemptionVault.setOverrideGetTokenRate(true); + await redemptionVault.setGetTokenRateValue(0); + + await expect( + redemptionVault.convertUsdToTokenTest( + 1, + redemptionVault.address, + 0, + ), + ).to.be.revertedWithCustomError(redemptionVault, 'InvalidTokenRate'); + }); + + it('when tokenRate == 0 but override rate is not 0', async () => { + const { redemptionVault, stableCoins, owner, dataFeed } = + await loadRvFixture(); + + await redemptionVault.setOverrideGetTokenRate(true); + await redemptionVault.setGetTokenRateValue(0); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + expect( + ( + await redemptionVault.convertUsdToTokenTest( + 0, + stableCoins.dai.address, + 1, + ) + ).amountToken, + ).eq(0); + }); + + it('when payment token is not setup and override rate is not 0', async () => { + const { redemptionVault } = await loadRvFixture(); + + await redemptionVault.setOverrideGetTokenRate(true); + await redemptionVault.setGetTokenRateValue(0); + + expect( + ( + await redemptionVault.convertUsdToTokenTest( + 0, + constants.AddressZero, + 1, + ) + ).amountToken, + ).eq(0); + }); + + it('should fail: when unknwon payment token', async () => { + const { redemptionVault } = await loadRvFixture(); + + await expect( + redemptionVault.convertUsdToTokenTest(1, constants.AddressZero, 0), + ).to.be.revertedWithoutReason(); + }); + }); + + describe('_convertMTokenToUsd', () => { + it('when amountMToken == 0', async () => { + const { redemptionVault } = await loadRvFixture(); + + expect( + (await redemptionVault.convertMTokenToUsdTest(0, 0)).amountUsd, + ).eq(0); + }); + + it('should fail: when override rate == 0', async () => { + const { redemptionVault } = await loadRvFixture(); + + await redemptionVault.setOverrideGetTokenRate(true); + await redemptionVault.setGetTokenRateValue(0); + + await expect( + redemptionVault.convertMTokenToUsdTest(1, 0), + ).to.be.revertedWithCustomError(redemptionVault, 'InvalidTokenRate'); + }); + }); + + describe('_calculateHoldbackPartRateFromAvg', () => { + it('returns 0 when target total value is not above instant part (equality)', async () => { + const { redemptionVault } = await loadRvFixture(); + const amountMToken = parseUnits('50'); + const amountMTokenInstant = parseUnits('50'); + const avgMTokenRate = parseUnits('1'); + const mTokenRate = parseUnits('200'); + const expected = expectedHoldbackPartRateFromAvg( + BigInt(amountMToken.toString()), + BigInt(amountMTokenInstant.toString()), + BigInt(mTokenRate.toString()), + BigInt(avgMTokenRate.toString()), + ); + expect(expected).eq(0n); + expect( + await redemptionVault.calculateHoldbackPartRateFromAvgTest( + amountMToken, + amountMTokenInstant, + mTokenRate, + avgMTokenRate, + ), + ).eq(expected.toString()); + }); + + it('returns 0 when avg rate implies lower target value than instant leg', async () => { + const { redemptionVault } = await loadRvFixture(); + const amountMToken = parseUnits('100'); + const amountMTokenInstant = parseUnits('10'); + const avgMTokenRate = parseUnits('1'); + const mTokenRate = parseUnits('2000'); + const expected = expectedHoldbackPartRateFromAvg( + BigInt(amountMToken.toString()), + BigInt(amountMTokenInstant.toString()), + BigInt(mTokenRate.toString()), + BigInt(avgMTokenRate.toString()), + ); + expect(expected).eq(0n); + expect( + await redemptionVault.calculateHoldbackPartRateFromAvgTest( + amountMToken, + amountMTokenInstant, + mTokenRate, + avgMTokenRate, + ), + ).eq(expected.toString()); + }); + + it('returns 0 when amountMTokenInstant is 0 and avgMTokenRate is 0', async () => { + const { redemptionVault } = await loadRvFixture(); + const amountMToken = parseUnits('100'); + const one = parseUnits('1'); + const expected = expectedHoldbackPartRateFromAvg( + BigInt(amountMToken.toString()), + 0n, + BigInt(one.toString()), + 0n, + ); + expect( + await redemptionVault.calculateHoldbackPartRateFromAvgTest( + amountMToken, + 0, + one, + 0, + ), + ).eq(expected.toString()); + }); + + it('returns 0 when amountMTokenInstant is 0', async () => { + const { redemptionVault } = await loadRvFixture(); + const amountMToken = parseUnits('100'); + const avgMTokenRate = parseUnits('1.25'); + const mTokenRate = parseUnits('1'); + const expected = expectedHoldbackPartRateFromAvg( + BigInt(amountMToken.toString()), + 0n, + BigInt(mTokenRate.toString()), + BigInt(avgMTokenRate.toString()), + ); + expect(expected).eq(0n); + expect( + await redemptionVault.calculateHoldbackPartRateFromAvgTest( + amountMToken, + 0, + mTokenRate, + avgMTokenRate, + ), + ).eq(expected.toString()); + }); + + it('applies integer rounding on the final rate', async () => { + const { redemptionVault } = await loadRvFixture(); + const amountMToken = 3n; + const amountMTokenInstant = 1n; + const mTokenRate = 3n; + const avgMTokenRate = 2n; + const expected = expectedHoldbackPartRateFromAvg( + amountMToken, + amountMTokenInstant, + mTokenRate, + avgMTokenRate, + ); + expect( + await redemptionVault.calculateHoldbackPartRateFromAvgTest( + amountMToken, + amountMTokenInstant, + mTokenRate, + avgMTokenRate, + ), + ).eq(expected.toString()); + }); + + it('succeeds with amountMToken == 0 when branch returns 0 before division', async () => { + const { redemptionVault } = await loadRvFixture(); + const amountMTokenInstant = parseUnits('100'); + const mTokenRate = parseUnits('10'); + const avgMTokenRate = parseUnits('1'); + const expected = expectedHoldbackPartRateFromAvg( + 0n, + BigInt(amountMTokenInstant.toString()), + BigInt(mTokenRate.toString()), + BigInt(avgMTokenRate.toString()), + ); + expect(expected).eq(0n); + expect( + await redemptionVault.calculateHoldbackPartRateFromAvgTest( + 0, + amountMTokenInstant, + mTokenRate, + avgMTokenRate, + ), + ).eq('0'); + }); + + it('reverts when amountMToken == 0 but holdback part would be positive (division by zero)', async () => { + const { redemptionVault } = await loadRvFixture(); + const amountMTokenInstant = parseUnits('100'); + const avgMTokenRate = parseUnits('2'); + const mTokenRate = parseUnits('1'); + await expect( + redemptionVault.calculateHoldbackPartRateFromAvgTest( + 0, + amountMTokenInstant, + mTokenRate, + avgMTokenRate, + ), + ).to.be.reverted; + }); + + it('matches reference for mixed instant and holdback with realistic WAD rates', async () => { + const { redemptionVault } = await loadRvFixture(); + const amountMToken = parseUnits('70'); + const amountMTokenInstant = parseUnits('30'); + const avgMTokenRate = parseUnits('1'); + const mTokenRate = parseUnits('1'); + const expected = expectedHoldbackPartRateFromAvg( + BigInt(amountMToken.toString()), + BigInt(amountMTokenInstant.toString()), + BigInt(mTokenRate.toString()), + BigInt(avgMTokenRate.toString()), + ); + expect( + await redemptionVault.calculateHoldbackPartRateFromAvgTest( + amountMToken, + amountMTokenInstant, + mTokenRate, + avgMTokenRate, + ), + ).eq(expected.toString()); + }); + + it('handles large values without overflow when inputs are bounded', async () => { + const { redemptionVault } = await loadRvFixture(); + const amountMToken = 10n ** 30n * 6n; + const amountMTokenInstant = 10n ** 30n * 4n; + const avgMTokenRate = 10n ** 18n * 2n; + const mTokenRate = 10n ** 18n * 5n; + const expected = expectedHoldbackPartRateFromAvg( + amountMToken, + amountMTokenInstant, + mTokenRate, + avgMTokenRate, + ); + expect( + await redemptionVault.calculateHoldbackPartRateFromAvgTest( + amountMToken, + amountMTokenInstant, + mTokenRate, + avgMTokenRate, + ), + ).eq(expected.toString()); + }); + }); + + describe('_calcAndValidateRedeem', () => { + it('should fail: when amountMTokenIn == 0', async () => { + const { redemptionVault, stableCoins, owner, dataFeed } = + await loadRvFixture(); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await expect( + redemptionVault.calcAndValidateRedeemTest( + constants.AddressZero, + stableCoins.dai.address, + 0, + 0, + 0, + false, + 0, + false, + ), + ).to.be.revertedWithCustomError(redemptionVault, 'InvalidAmount'); + }); + + it('should override fee percent', async () => { + const { + redemptionVault, + stableCoins, + owner, + dataFeed, + mockedAggregatorMToken, + } = await loadRvFixture(); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + const result = + await redemptionVault.callStatic.calcAndValidateRedeemTest( + constants.AddressZero, + stableCoins.dai.address, + parseUnits('100'), + 0, + 0, + true, + 10_00, + false, + ); + + expect(result.feeAmount).eq(parseUnits('10')); + expect(result.amountTokenOutWithoutFee).eq(parseUnits('90')); + }); + + it('should override token out rate and fee percent', async () => { + const { + redemptionVault, + stableCoins, + owner, + dataFeed, + mockedAggregatorMToken, + } = await loadRvFixture(); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + await setRoundData({ mockedAggregator: mockedAggregatorMToken }, 1); + + const result = + await redemptionVault.callStatic.calcAndValidateRedeemTest( + constants.AddressZero, + stableCoins.dai.address, + parseUnits('100'), + 0, + parseUnits('2'), + true, + 10_00, + false, + ); + + expect(result.feeAmount).eq(parseUnits('5')); + expect(result.amountTokenOutWithoutFee).eq(parseUnits('45')); + }); + + it('should override token out rate, mtoken rate and fee percent', async () => { + const { redemptionVault, stableCoins, owner, dataFeed } = + await loadRvFixture(); + + await addPaymentTokenTest( + { vault: redemptionVault, owner }, + stableCoins.dai, + dataFeed.address, + 0, + true, + ); + + const result = + await redemptionVault.callStatic.calcAndValidateRedeemTest( + constants.AddressZero, + stableCoins.dai.address, + parseUnits('100'), + parseUnits('1'), + parseUnits('2'), + true, + 10_00, + false, + ); + + expect(result.feeAmount).eq(parseUnits('5')); + expect(result.amountTokenOutWithoutFee).eq(parseUnits('45')); + }); + }); + }); + + otherTests(rvFixture); + }); +};