|
| 1 | +# FCM Architecture |
| 2 | + |
| 3 | +This repo implements an [ERC4626](https://eips.ethereum.org/EIPS/eip-4626)-compliant vault which implements a levered investment with automated rebalancing. |
| 4 | + |
| 5 | +## Terminology |
| 6 | +- **Asset** - ERC4626 term meaning "the unit of account of this vault". Deposits, withdrawals, and NAV for a vault are denominated in the vault's asset. The asset must be an ERC20 token. |
| 7 | + - **InnerAsset/OuterAsset** - The asset of the inner vault or outer vault, respective (see below) |
| 8 | +- **Share** - ERC4626 term meaning "a portion of the total assets in this vault". Shares are fungible and are represented as ERC20 tokens. Vault users deposit assets and receive shares. |
| 9 | + - **InnerShare/OuterShare** - The share of the inner vault or outer vault, respective (see below) |
| 10 | +- **Outer Vault** - The ERC4626 vault implemented in this repository, which borrows against deposits to invest in an inner vault. |
| 11 | +- **Inner Vault** - The ERC4626 vault which the outer vault invests borrowing proceeds in. |
| 12 | + |
| 13 | +## Dependencies |
| 14 | +### Lending Protocol |
| 15 | +[Morpho Blue](https://github.com/morpho-org/morpho-blue) |
| 16 | + |
| 17 | +### Automated Market Maker (AMM) |
| 18 | +[FlowSwap (Uniswap v3)](https://flowswap.io/) |
| 19 | + |
| 20 | +### Inner Vault |
| 21 | +In general, the inner vault may be any ERC4626-compliant vault. As an example, Jon's FUSDEV vault uses [Morpho Vault v2](https://docs.morpho.org/build/earn/concepts/vault-mechanics) |
| 22 | + |
| 23 | +**Liquidity:** We must assume that the inner vault MAY be unable to satisfy any withdrawal requests, at any time (eg. is illiquid). To address this in a general way, we primarily use DEX swaps to acquire/dispose of InnerShares. We then rely on the DEX to provide sufficient liquidity for the shares. |
| 24 | + |
| 25 | +**NAV Reporting:** We must assume that the NAV (share price) reported by the vault may be out of date on the order of days. |
| 26 | + |
| 27 | +## Deposit Flow |
| 28 | +### A. AMM-Mediated Deposit |
| 29 | +We swap debt tokens (InnerAsset) to InnerShares via an AMM. Our ability to satisfy deposits is dependent on available liquidity in the AMM pool. |
| 30 | + |
| 31 | +```mermaid |
| 32 | +sequenceDiagram |
| 33 | + autonumber |
| 34 | + actor User |
| 35 | + participant Outer as Outer ERC4626 Vault |
| 36 | + participant Lender as Lending Protocol |
| 37 | + participant Dex as AMM |
| 38 | +
|
| 39 | + User->>Outer: deposit(outerAsset) |
| 40 | + activate Outer |
| 41 | + |
| 42 | + Outer->>Lender: supply (outerAsset) |
| 43 | + Lender-->>Outer: borrow (innerAsset) |
| 44 | + Note over Lender,Outer: Always supply all deposits. <br />Borrow amount limited by LTV. |
| 45 | +
|
| 46 | + Outer->>Dex: swap (innerAsset → innerShare) |
| 47 | + Dex-->>Outer: innerShare |
| 48 | + |
| 49 | + Outer-->>User: outerShare |
| 50 | + deactivate Outer |
| 51 | +``` |
| 52 | + |
| 53 | +### B. Direct Deposit |
| 54 | +We deposit debt tokens (InnerAsset) to InnerShares via the inner vault's `deposit` function. Our ability to satisfy deposits is dependent on the vault's deposit capacity ([`maxDeposit`](https://ethereum.org/developers/docs/standards/tokens/erc-4626/#maxdeposit)) |
| 55 | +```mermaid |
| 56 | +sequenceDiagram |
| 57 | + autonumber |
| 58 | + actor User |
| 59 | + participant Outer as Outer ERC4626 Vault |
| 60 | + participant Lender as Lending Protocol |
| 61 | + participant Inner as Inner ERC4626 Vault |
| 62 | +
|
| 63 | + User->>Outer: deposit(outerAsset) |
| 64 | + activate Outer |
| 65 | +
|
| 66 | + Outer->>Lender: supply (outerAsset) |
| 67 | + Lender-->>Outer: borrow (innerAsset) |
| 68 | + Note over Lender,Outer: Always supply all deposits. <br />Borrow amount limited by LTV. |
| 69 | +
|
| 70 | + Outer->>Inner: deposit (innerAsset) |
| 71 | + activate Inner |
| 72 | + Inner-->>Outer: innerShare |
| 73 | + deactivate Inner |
| 74 | +
|
| 75 | + Outer-->>User: outerShare |
| 76 | + deactivate Outer |
| 77 | +``` |
| 78 | + |
| 79 | +## Withdrawal Flow |
| 80 | +There are several ways to implement withdrawals, enumerated below. The main differences are: |
| 81 | +1. Source of liquidity risk (inner vault vs AMM). |
| 82 | +2. Ability to withdraw full amount when LTV is near limit. In option C, the flashloan enables always repaying the full debt amount first. In options A/B, we may be unable to do this (depending on LTV). See [below](#ltv-limit-edge-case) for details. |
| 83 | + |
| 84 | +### A. AMM-Mediated Withdrawal |
| 85 | +```mermaid |
| 86 | +sequenceDiagram |
| 87 | + autonumber |
| 88 | + actor User |
| 89 | + participant Outer as Outer ERC4626 Vault |
| 90 | + participant Lender as Lending Protocol |
| 91 | + participant Dex as AMM |
| 92 | +
|
| 93 | + User->>Outer: redeem(outerShare) |
| 94 | + activate Outer |
| 95 | +
|
| 96 | + Outer->>Dex: swap (innerShare → innerAsset) |
| 97 | + Dex-->>Outer: innerAsset |
| 98 | + Note over Outer,Dex: We realize market price, not NAV<br /> (NAV may be higher or lower) |
| 99 | +
|
| 100 | + Outer->>Lender: repay (innerAsset) |
| 101 | + Lender-->>Outer: withdraw collateral (outerAsset) |
| 102 | +
|
| 103 | + Outer->>Dex: reconcile surplus (innerAsset → outerAsset) |
| 104 | + Dex-->>Outer: outerAsset |
| 105 | +
|
| 106 | + Outer-->>User: outerAsset |
| 107 | + deactivate Outer |
| 108 | +``` |
| 109 | + |
| 110 | +#### Pros |
| 111 | + - No inner vault liquidity risk |
| 112 | + |
| 113 | +#### Cons |
| 114 | + - Requires repaying debt before withdrawing collateral. Reverts if the yield→debt swap underdelivers and the intermediate HF would dip below 1. |
| 115 | + - Pays DEX fees/slippage |
| 116 | + - Pool liquidity risk: thin yield/debt pool degrades or blocks large redeems. |
| 117 | + |
| 118 | +### B. Direct Withdrawal |
| 119 | +```mermaid |
| 120 | +sequenceDiagram |
| 121 | + autonumber |
| 122 | + actor User |
| 123 | + participant Outer as Outer ERC4626 Vault |
| 124 | + participant Lender as Lending Protocol |
| 125 | + participant Inner as Inner ERC4626 Vault |
| 126 | +
|
| 127 | + User->>Outer: redeem(outerShare) |
| 128 | + activate Outer |
| 129 | + |
| 130 | + Outer->>Inner: redeem (innerShare) |
| 131 | + activate Inner |
| 132 | + Inner-->>Outer: innerAsset |
| 133 | + deactivate Inner |
| 134 | +
|
| 135 | + Outer->>Lender: repay (innerAsset) |
| 136 | + Lender-->>Outer: withdraw collateral (outerAsset) |
| 137 | +
|
| 138 | + Outer->>Dex: reconcile surplus (outerAsset ↔ innerAsset) |
| 139 | + Dex-->>Outer: innerAsset |
| 140 | + |
| 141 | + Outer-->>User: outerAsset |
| 142 | + deactivate Outer |
| 143 | +``` |
| 144 | + |
| 145 | +#### Pros |
| 146 | + - Redeems yield at NAV — no LP fee, no slippage on the yield leg. |
| 147 | + - Independent on AMM liquidity for the yield asset. |
| 148 | + |
| 149 | +#### Cons |
| 150 | + - Dependent on available liquidity in inner vault. |
| 151 | + |
| 152 | +### C. Flash Loan Path |
| 153 | +```mermaid |
| 154 | +sequenceDiagram |
| 155 | + autonumber |
| 156 | + actor User |
| 157 | + participant Outer as Outer ERC4626 Vault |
| 158 | + participant Lender as Lending Protocol |
| 159 | + participant Dex as AMM |
| 160 | +
|
| 161 | + User->>Outer: redeem(outerShare) |
| 162 | + activate Outer |
| 163 | +
|
| 164 | + Lender-->>Outer: flashloan (innerAsset) |
| 165 | +
|
| 166 | + Outer->>Lender: repay (innerAsset) |
| 167 | + Lender-->>Outer: withdraw collateral (outerAsset) |
| 168 | +
|
| 169 | + Outer->>Dex: swap (yieldAsset → innerAsset) |
| 170 | + Dex-->>Outer: innerAsset |
| 171 | +
|
| 172 | + Outer->>Dex: reconcile surplus (outerAsset ↔ innerAsset) |
| 173 | + Dex-->>Outer: innerAsset |
| 174 | +
|
| 175 | + Outer->>Lender: repay flashloan (innerAsset) |
| 176 | +
|
| 177 | + Outer-->>User: outerAsset |
| 178 | + deactivate Outer |
| 179 | +``` |
| 180 | + |
| 181 | +#### Pros |
| 182 | + - Deterministic unwind at any HF — debt is cleared before collateral moves |
| 183 | + |
| 184 | +#### Cons |
| 185 | + - Most complex: callback-based reentry, encoded calldata, extra Morpho roundtrip. |
| 186 | + - Larger attack surface — callback must validate msg.sender and decode data correctly. |
| 187 | + - Still depends on DEX for the yield sale and reconcile legs (liquidity, fees/slippage) |
| 188 | + |
| 189 | +#### LTV Limit Edge Case |
| 190 | +```mermaid |
| 191 | +sequenceDiagram |
| 192 | + autonumber |
| 193 | + actor User |
| 194 | + participant Outer as Outer ERC4626 Vault |
| 195 | + participant Lender as Lending Protocol |
| 196 | + participant Dex as AMM |
| 197 | + |
| 198 | + Note over Lender: Initial: HF ≈ 1 (LTV near LLTV)<br/>In options A/B, if our recovered outerAsset is < debt<br />then we can't repay full debt amount. |
| 199 | +
|
| 200 | + User->>Outer: redeem(outerShare) |
| 201 | + activate Outer |
| 202 | +
|
| 203 | + Lender-->>Outer: flashloan (innerAsset) |
| 204 | + Note over Outer,Lender: Vault holds enough debt-token to repay in full<br/>without touching the position |
| 205 | +
|
| 206 | + Outer->>Lender: repay full debtSlice (innerAsset) |
| 207 | + Note over Lender: HF improves (debt ↓, coll unchanged)<br/>Position is over-collateralized |
| 208 | +
|
| 209 | + Lender-->>Outer: withdraw collSlice (outerAsset) |
| 210 | + Note over Lender: Always succeeds because we just repaid full debt slice |
| 211 | +
|
| 212 | + Outer->>Dex: sell yieldSlice → innerAsset |
| 213 | + Dex-->>Outer: innerAsset (may be < flash loan amount) |
| 214 | +
|
| 215 | + alt surplus (yield sale ≥ flash) |
| 216 | + Outer->>Dex: swap surplus innerAsset → outerAsset |
| 217 | + Dex-->>Outer: extra outerAsset (user bonus) |
| 218 | + else deficit (yield sale < flash) |
| 219 | + Outer->>Dex: swap some collSlice → innerAsset |
| 220 | + Dex-->>Outer: innerAsset (covers deficit) |
| 221 | + Note over Outer: User absorbs their own<br/>yield shortfall at market price |
| 222 | + end |
| 223 | +
|
| 224 | + Outer->>Lender: repay flashloan (innerAsset) |
| 225 | +
|
| 226 | + Outer-->>User: outerAsset |
| 227 | + deactivate Outer |
| 228 | +``` |
| 229 | + |
| 230 | +## Rebalancing |
| 231 | +See **TODO LINK TO REBALANCING SPEC** |
| 232 | + |
| 233 | +## Custom Behaviour |
| 234 | +See [here](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol#L50-L68) for guidance on how to safely extend the base ERC4626 contract. |
| 235 | + |
| 236 | +## Security |
| 237 | +### Donation/Inflation Attack |
| 238 | +See [explanation from OpenZeppelin](https://docs.openzeppelin.com/contracts/5.x/erc4626#security-concern-inflation-attack). |
| 239 | + |
| 240 | +Our implementation is safe from this attack because we inherit from the OpenZeppelin ERC4626 base contract, which implements a virtual share mitigation. See [here](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol#L22-L47) for guidance on extending this mitigation. |
| 241 | + |
| 242 | +### Re-entrancy Attack (TODO) |
| 243 | +For each external function, how does it protect against re-entrancy? |
| 244 | + |
| 245 | +### Sandwich Attack |
| 246 | +An attacker manipulates AMM prices before and after our swap to capture part of the value of our swap. |
| 247 | +- The primary mitigation is a slippage limit, which limits how much slippage we will accept on each trade. This doesn't prevent the attack, but does limit how much value can be extracted per trade. |
| 248 | +- Flow as the underlying platform provides some protection. There is no system akin to [MEV-Boost](https://github.com/flashbots/mev-boost), which systematizes MEV extraction. No individual node in Flow can deterministically dictate transaction ordering. Attackers need to send many transactions, hope some are placed in the desired order, and be able to revert operations on those that are not in the desired order. Still possible, but more complex and expensive. |
| 249 | + |
| 250 | +If an attacker is able to invoke a function which performs a swap (that isn't swapping their funds), then the sandwich attack becomes much more dangerous (eg. a permissionless `rebalance` function). |
| 251 | +- The attacker can reliably order their operations by structuring the "full sandwich" as one transaction. |
| 252 | +- The attack is repeatable. |
| 253 | + |
| 254 | +### Oracle Manipulation (TODO) |
| 255 | + |
| 256 | +## Dust Strategy (TODO) |
| 257 | + |
| 258 | +## References / Prior Art |
| 259 | + |
| 260 | +- [Patrick's Vault PoC](https://github.com/holyfuchs/fcm-sol-poc) |
| 261 | +- [Schlagonia Morpho Lender Vault](https://github.com/Schlagonia/lender-borrower/blob/morpho/src/MorphoBlueLenderBorrower.sol) |
0 commit comments