diff --git a/README.md b/README.md index f5b8804..872c9b5 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Two facilitators exist today, covering the two ends of a fund DEPOSIT order's li | `CommitDeposit` | Order creation | pull bridge funds → create DEPOSIT order → commit | | `MorphoAllocator` | Order maturation | unlock → rebalance Morpho Vault V2 → deposit collateral & borrow | +`src/checks/` additionally holds permissionless, read-only guard contracts meant to be called as the first step of a batch transaction to make the rest of the batch revert on stale state; they hold no roles and no storage. + ## CommitDeposit (`src/CommitDeposit.sol`) `run(intentId, pullAmount, commitAmount, minSharesOut)`: @@ -34,6 +36,27 @@ Two facilitators exist today, covering the two ends of a fund DEPOSIT order's li Success emits `Allocated(intentId, unlocked, allocateAdapter, gatheredTotal, borrowAmount)`. +## DepositCapStateGuard (`src/checks/DepositCapStateGuard.sol`) + +`requireState(facility, checks)` — a stateless, read-only guard that reverts unless every intent in `checks` matches the caller's expectations: + +- **Deposit cap** must equal `expectedCap` exactly (`StaleCap` otherwise) — cap changes are precisely the state transitions the guard exists to catch. +- **Total supply** must lie within the inclusive `[minSupply, maxSupply]` range (`SupplyOutOfRange` otherwise), so benign concurrent deposits/withdrawals inside the tolerated band do not fail the batch. Pass `minSupply == maxSupply` for an exact check; an inverted range reverts with `InvalidSupplyRange`. + +**Usage: this contract must be called as the FIRST transaction of a batch** (e.g. an EIP-7702 batch) executed in the mode where any failing call reverts the whole batch. The batch calls this read-only function first; if the on-chain cap/supply state no longer matches what was expected when the batch was signed, the guard reverts and the rest of the batch reverts with it, so no state-changing call executes against stale state. + +The guard is unpermissioned (anyone may call it with any arguments), holds no storage, and takes the Facility as a call parameter, so one deployment serves any Facility. + +### Deployment + +`script/DeployDepositCapStateGuard.s.sol` deploys the guard through the canonical CREATE2 factory (`0x4e59b44847b379578588920cA78FbF26c0B4956C`) with a salt ground for a `0x3fa77e57` ("3F ATTEST") vanity prefix, giving the same address on every chain: + +``` +DepositCapStateGuard: 0x3Fa77E57baC28940621d1aaE1a136058c4064623 +``` + +The script asserts the deployment lands on this address and reverts otherwise. The address is a pure function of the init code and the salt: re-grind the salt (and update the script constants and this README) if the guard source or the compiler profile changes. + ## Roles & trust assumptions - **Owner / executor.** Both contracts use Solady `OwnableRoles`. The owner (set at initialization) grants and revokes `EXECUTOR_ROLE`; only executors can call `run`. Executors are trusted operators: parameters like the pull/commit split, `depositAmount`, and the rebalance shape are their judgment calls — the contracts enforce invariants (order states, slippage minima, utilisation caps, balance audits), not strategy. diff --git a/broadcast/DeployDepositCapStateGuard.s.sol/1/run-1783338513185.json b/broadcast/DeployDepositCapStateGuard.s.sol/1/run-1783338513185.json new file mode 100644 index 0000000..51eb745 --- /dev/null +++ b/broadcast/DeployDepositCapStateGuard.s.sol/1/run-1783338513185.json @@ -0,0 +1,47 @@ +{ + "transactions": [ + { + "hash": "0x23cef0168b2aee10048138e696cde700a0673b2faf54ddaa677faf19df254b96", + "transactionType": "CREATE2", + "contractName": "DepositCapStateGuard", + "contractAddress": "0x3fa77e57bac28940621d1aae1a136058c4064623", + "function": null, + "arguments": null, + "transaction": { + "from": "0xabff377a1f7a0b061c9d4395b53f1488b153333f", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "gas": "0x575fc", + "value": "0x0", + "input": "0x7fde142edeac1c4c898e276a070df232a649cda0a4e930355b1f72abb019de8a608080604052346015576103b3908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c632a1620d914610024575f80fd5b3461024a57604036600319011261024a576004356001600160a01b0381169081900361024a5760243567ffffffffffffffff811161024a573660238201121561024a5780600401359067ffffffffffffffff821161024a573660248360071b8301011161024a575f5b828110156102f1578060071b82016024810160648201356084830135918282116102d85735916040519163bd85b03960e01b83528360048401526020836024818c5afa928315610269575f936102a5575b50808310801561029c575b6102745750505060405163906e277b60e01b8152816004820152610180816024818a5afa908115610269575f91610150575b50604001519160440135908183036101385750505060010161008d565b636e77284760e11b5f5260045260245260445260645ffd5b90506101803d8111610262575b61016781836102f3565b8101818103610180811261024a576101201361024a576040519060e082019082821067ffffffffffffffff83111761024e576101b5916040526101aa8185610336565b835260408401610336565b6020820152608082015160408201526101d060a08301610315565b606082015260c082015164ffffffffff8116810361024a57608082015260e08201519060ff8216820361024a5761024261016060449460409460a085015261021b6101008201610329565b60c085015261022d6101208201610315565b5061023b6101408201610315565b5001610329565b50915061011b565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b503d61015d565b6040513d5f823e3d90fd5b608494506040519363e016ae5560e01b85526004850152602484015260448301526064820152fd5b508183116100e9565b9092506020813d82116102d0575b816102c0602093836102f3565b8101031261024a5751915f6100de565b3d91506102b3565b635e4aefe960e01b5f523560045260245260445260645ffd5b005b90601f8019910116810190811067ffffffffffffffff82111761024e57604052565b51906001600160a01b038216820361024a57565b5190811515820361024a57565b919082604091031261024a576040516040810181811067ffffffffffffffff82111761024e57604052602061037881839561037081610315565b855201610329565b91015256fea26469706673582212200cf1aea5547025eafdd9a16c8ed54a7ff01a00dbcc5c313365d3208df9a9ff4464736f6c634300081c0033", + "nonce": "0x231", + "chainId": "0x1" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [ + { + "type": "0x2", + "status": "0x1", + "cumulativeGasUsed": "0x21fc964", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "transactionHash": "0x23cef0168b2aee10048138e696cde700a0673b2faf54ddaa677faf19df254b96", + "transactionIndex": "0xde", + "blockHash": "0xa8df0e30539a0a0044a1a2dc9d6801580c680760a94548f26e0dde2ec6ce061d", + "blockNumber": "0x184b10c", + "gasUsed": "0x3f41f", + "effectiveGasPrice": "0x6a31b99", + "from": "0xabff377a1f7a0b061c9d4395b53f1488b153333f", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "contractAddress": "0x3fa77e57bac28940621d1aae1a136058c4064623" + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1783338513185, + "chain": 1, + "commit": "7ed90f5" +} \ No newline at end of file diff --git a/broadcast/DeployDepositCapStateGuard.s.sol/1/run-1783338539903.json b/broadcast/DeployDepositCapStateGuard.s.sol/1/run-1783338539903.json new file mode 100644 index 0000000..f41c1ce --- /dev/null +++ b/broadcast/DeployDepositCapStateGuard.s.sol/1/run-1783338539903.json @@ -0,0 +1,47 @@ +{ + "transactions": [ + { + "hash": "0x23cef0168b2aee10048138e696cde700a0673b2faf54ddaa677faf19df254b96", + "transactionType": "CREATE2", + "contractName": "DepositCapStateGuard", + "contractAddress": "0x3fa77e57bac28940621d1aae1a136058c4064623", + "function": null, + "arguments": null, + "transaction": { + "from": "0xabff377a1f7a0b061c9d4395b53f1488b153333f", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "gas": "0x575fc", + "value": "0x0", + "input": "0x7fde142edeac1c4c898e276a070df232a649cda0a4e930355b1f72abb019de8a608080604052346015576103b3908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c632a1620d914610024575f80fd5b3461024a57604036600319011261024a576004356001600160a01b0381169081900361024a5760243567ffffffffffffffff811161024a573660238201121561024a5780600401359067ffffffffffffffff821161024a573660248360071b8301011161024a575f5b828110156102f1578060071b82016024810160648201356084830135918282116102d85735916040519163bd85b03960e01b83528360048401526020836024818c5afa928315610269575f936102a5575b50808310801561029c575b6102745750505060405163906e277b60e01b8152816004820152610180816024818a5afa908115610269575f91610150575b50604001519160440135908183036101385750505060010161008d565b636e77284760e11b5f5260045260245260445260645ffd5b90506101803d8111610262575b61016781836102f3565b8101818103610180811261024a576101201361024a576040519060e082019082821067ffffffffffffffff83111761024e576101b5916040526101aa8185610336565b835260408401610336565b6020820152608082015160408201526101d060a08301610315565b606082015260c082015164ffffffffff8116810361024a57608082015260e08201519060ff8216820361024a5761024261016060449460409460a085015261021b6101008201610329565b60c085015261022d6101208201610315565b5061023b6101408201610315565b5001610329565b50915061011b565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b503d61015d565b6040513d5f823e3d90fd5b608494506040519363e016ae5560e01b85526004850152602484015260448301526064820152fd5b508183116100e9565b9092506020813d82116102d0575b816102c0602093836102f3565b8101031261024a5751915f6100de565b3d91506102b3565b635e4aefe960e01b5f523560045260245260445260645ffd5b005b90601f8019910116810190811067ffffffffffffffff82111761024e57604052565b51906001600160a01b038216820361024a57565b5190811515820361024a57565b919082604091031261024a576040516040810181811067ffffffffffffffff82111761024e57604052602061037881839561037081610315565b855201610329565b91015256fea26469706673582212200cf1aea5547025eafdd9a16c8ed54a7ff01a00dbcc5c313365d3208df9a9ff4464736f6c634300081c0033", + "nonce": "0x231", + "chainId": "0x1" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [ + { + "type": "0x2", + "status": "0x1", + "cumulativeGasUsed": "0x21fc964", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "transactionHash": "0x23cef0168b2aee10048138e696cde700a0673b2faf54ddaa677faf19df254b96", + "transactionIndex": "0xde", + "blockHash": "0xa8df0e30539a0a0044a1a2dc9d6801580c680760a94548f26e0dde2ec6ce061d", + "blockNumber": "0x184b10c", + "gasUsed": "0x3f41f", + "effectiveGasPrice": "0x6a31b99", + "from": "0xabff377a1f7a0b061c9d4395b53f1488b153333f", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "contractAddress": "0x3fa77e57bac28940621d1aae1a136058c4064623" + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1783338539903, + "chain": 1, + "commit": "7ed90f5" +} \ No newline at end of file diff --git a/broadcast/DeployDepositCapStateGuard.s.sol/1/run-latest.json b/broadcast/DeployDepositCapStateGuard.s.sol/1/run-latest.json new file mode 100644 index 0000000..f41c1ce --- /dev/null +++ b/broadcast/DeployDepositCapStateGuard.s.sol/1/run-latest.json @@ -0,0 +1,47 @@ +{ + "transactions": [ + { + "hash": "0x23cef0168b2aee10048138e696cde700a0673b2faf54ddaa677faf19df254b96", + "transactionType": "CREATE2", + "contractName": "DepositCapStateGuard", + "contractAddress": "0x3fa77e57bac28940621d1aae1a136058c4064623", + "function": null, + "arguments": null, + "transaction": { + "from": "0xabff377a1f7a0b061c9d4395b53f1488b153333f", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "gas": "0x575fc", + "value": "0x0", + "input": "0x7fde142edeac1c4c898e276a070df232a649cda0a4e930355b1f72abb019de8a608080604052346015576103b3908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c632a1620d914610024575f80fd5b3461024a57604036600319011261024a576004356001600160a01b0381169081900361024a5760243567ffffffffffffffff811161024a573660238201121561024a5780600401359067ffffffffffffffff821161024a573660248360071b8301011161024a575f5b828110156102f1578060071b82016024810160648201356084830135918282116102d85735916040519163bd85b03960e01b83528360048401526020836024818c5afa928315610269575f936102a5575b50808310801561029c575b6102745750505060405163906e277b60e01b8152816004820152610180816024818a5afa908115610269575f91610150575b50604001519160440135908183036101385750505060010161008d565b636e77284760e11b5f5260045260245260445260645ffd5b90506101803d8111610262575b61016781836102f3565b8101818103610180811261024a576101201361024a576040519060e082019082821067ffffffffffffffff83111761024e576101b5916040526101aa8185610336565b835260408401610336565b6020820152608082015160408201526101d060a08301610315565b606082015260c082015164ffffffffff8116810361024a57608082015260e08201519060ff8216820361024a5761024261016060449460409460a085015261021b6101008201610329565b60c085015261022d6101208201610315565b5061023b6101408201610315565b5001610329565b50915061011b565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b503d61015d565b6040513d5f823e3d90fd5b608494506040519363e016ae5560e01b85526004850152602484015260448301526064820152fd5b508183116100e9565b9092506020813d82116102d0575b816102c0602093836102f3565b8101031261024a5751915f6100de565b3d91506102b3565b635e4aefe960e01b5f523560045260245260445260645ffd5b005b90601f8019910116810190811067ffffffffffffffff82111761024e57604052565b51906001600160a01b038216820361024a57565b5190811515820361024a57565b919082604091031261024a576040516040810181811067ffffffffffffffff82111761024e57604052602061037881839561037081610315565b855201610329565b91015256fea26469706673582212200cf1aea5547025eafdd9a16c8ed54a7ff01a00dbcc5c313365d3208df9a9ff4464736f6c634300081c0033", + "nonce": "0x231", + "chainId": "0x1" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [ + { + "type": "0x2", + "status": "0x1", + "cumulativeGasUsed": "0x21fc964", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "transactionHash": "0x23cef0168b2aee10048138e696cde700a0673b2faf54ddaa677faf19df254b96", + "transactionIndex": "0xde", + "blockHash": "0xa8df0e30539a0a0044a1a2dc9d6801580c680760a94548f26e0dde2ec6ce061d", + "blockNumber": "0x184b10c", + "gasUsed": "0x3f41f", + "effectiveGasPrice": "0x6a31b99", + "from": "0xabff377a1f7a0b061c9d4395b53f1488b153333f", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "contractAddress": "0x3fa77e57bac28940621d1aae1a136058c4064623" + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1783338539903, + "chain": 1, + "commit": "7ed90f5" +} \ No newline at end of file diff --git a/broadcast/DeployDepositCapStateGuard.s.sol/11155111/run-1783338690064.json b/broadcast/DeployDepositCapStateGuard.s.sol/11155111/run-1783338690064.json new file mode 100644 index 0000000..5e8ae14 --- /dev/null +++ b/broadcast/DeployDepositCapStateGuard.s.sol/11155111/run-1783338690064.json @@ -0,0 +1,47 @@ +{ + "transactions": [ + { + "hash": "0x125480eac10d5ae705868169409335bd3ee5247c9988bf72d35ccbf73ba3b2fb", + "transactionType": "CREATE2", + "contractName": "DepositCapStateGuard", + "contractAddress": "0x3fa77e57bac28940621d1aae1a136058c4064623", + "function": null, + "arguments": null, + "transaction": { + "from": "0x3f7e578d292233aa8bc811cebc357e33001c306c", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "gas": "0x575fc", + "value": "0x0", + "input": "0x7fde142edeac1c4c898e276a070df232a649cda0a4e930355b1f72abb019de8a608080604052346015576103b3908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c632a1620d914610024575f80fd5b3461024a57604036600319011261024a576004356001600160a01b0381169081900361024a5760243567ffffffffffffffff811161024a573660238201121561024a5780600401359067ffffffffffffffff821161024a573660248360071b8301011161024a575f5b828110156102f1578060071b82016024810160648201356084830135918282116102d85735916040519163bd85b03960e01b83528360048401526020836024818c5afa928315610269575f936102a5575b50808310801561029c575b6102745750505060405163906e277b60e01b8152816004820152610180816024818a5afa908115610269575f91610150575b50604001519160440135908183036101385750505060010161008d565b636e77284760e11b5f5260045260245260445260645ffd5b90506101803d8111610262575b61016781836102f3565b8101818103610180811261024a576101201361024a576040519060e082019082821067ffffffffffffffff83111761024e576101b5916040526101aa8185610336565b835260408401610336565b6020820152608082015160408201526101d060a08301610315565b606082015260c082015164ffffffffff8116810361024a57608082015260e08201519060ff8216820361024a5761024261016060449460409460a085015261021b6101008201610329565b60c085015261022d6101208201610315565b5061023b6101408201610315565b5001610329565b50915061011b565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b503d61015d565b6040513d5f823e3d90fd5b608494506040519363e016ae5560e01b85526004850152602484015260448301526064820152fd5b508183116100e9565b9092506020813d82116102d0575b816102c0602093836102f3565b8101031261024a5751915f6100de565b3d91506102b3565b635e4aefe960e01b5f523560045260245260445260645ffd5b005b90601f8019910116810190811067ffffffffffffffff82111761024e57604052565b51906001600160a01b038216820361024a57565b5190811515820361024a57565b919082604091031261024a576040516040810181811067ffffffffffffffff82111761024e57604052602061037881839561037081610315565b855201610329565b91015256fea26469706673582212200cf1aea5547025eafdd9a16c8ed54a7ff01a00dbcc5c313365d3208df9a9ff4464736f6c634300081c0033", + "nonce": "0x1a", + "chainId": "0xaa36a7" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [ + { + "type": "0x2", + "status": "0x1", + "cumulativeGasUsed": "0x9ad954", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "transactionHash": "0x125480eac10d5ae705868169409335bd3ee5247c9988bf72d35ccbf73ba3b2fb", + "transactionIndex": "0x47", + "blockHash": "0x07fb28c4387c8933091faf833382889d1cc23e6c6824443efa2b119a3dd139d5", + "blockNumber": "0xab2168", + "gasUsed": "0x3f41f", + "effectiveGasPrice": "0x3e78bc52", + "from": "0x3f7e578d292233aa8bc811cebc357e33001c306c", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "contractAddress": "0x3fa77e57bac28940621d1aae1a136058c4064623" + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1783338690064, + "chain": 11155111, + "commit": "7ed90f5" +} \ No newline at end of file diff --git a/broadcast/DeployDepositCapStateGuard.s.sol/11155111/run-1783338727468.json b/broadcast/DeployDepositCapStateGuard.s.sol/11155111/run-1783338727468.json new file mode 100644 index 0000000..42418f2 --- /dev/null +++ b/broadcast/DeployDepositCapStateGuard.s.sol/11155111/run-1783338727468.json @@ -0,0 +1,47 @@ +{ + "transactions": [ + { + "hash": "0x125480eac10d5ae705868169409335bd3ee5247c9988bf72d35ccbf73ba3b2fb", + "transactionType": "CREATE2", + "contractName": "DepositCapStateGuard", + "contractAddress": "0x3fa77e57bac28940621d1aae1a136058c4064623", + "function": null, + "arguments": null, + "transaction": { + "from": "0x3f7e578d292233aa8bc811cebc357e33001c306c", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "gas": "0x575fc", + "value": "0x0", + "input": "0x7fde142edeac1c4c898e276a070df232a649cda0a4e930355b1f72abb019de8a608080604052346015576103b3908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c632a1620d914610024575f80fd5b3461024a57604036600319011261024a576004356001600160a01b0381169081900361024a5760243567ffffffffffffffff811161024a573660238201121561024a5780600401359067ffffffffffffffff821161024a573660248360071b8301011161024a575f5b828110156102f1578060071b82016024810160648201356084830135918282116102d85735916040519163bd85b03960e01b83528360048401526020836024818c5afa928315610269575f936102a5575b50808310801561029c575b6102745750505060405163906e277b60e01b8152816004820152610180816024818a5afa908115610269575f91610150575b50604001519160440135908183036101385750505060010161008d565b636e77284760e11b5f5260045260245260445260645ffd5b90506101803d8111610262575b61016781836102f3565b8101818103610180811261024a576101201361024a576040519060e082019082821067ffffffffffffffff83111761024e576101b5916040526101aa8185610336565b835260408401610336565b6020820152608082015160408201526101d060a08301610315565b606082015260c082015164ffffffffff8116810361024a57608082015260e08201519060ff8216820361024a5761024261016060449460409460a085015261021b6101008201610329565b60c085015261022d6101208201610315565b5061023b6101408201610315565b5001610329565b50915061011b565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b503d61015d565b6040513d5f823e3d90fd5b608494506040519363e016ae5560e01b85526004850152602484015260448301526064820152fd5b508183116100e9565b9092506020813d82116102d0575b816102c0602093836102f3565b8101031261024a5751915f6100de565b3d91506102b3565b635e4aefe960e01b5f523560045260245260445260645ffd5b005b90601f8019910116810190811067ffffffffffffffff82111761024e57604052565b51906001600160a01b038216820361024a57565b5190811515820361024a57565b919082604091031261024a576040516040810181811067ffffffffffffffff82111761024e57604052602061037881839561037081610315565b855201610329565b91015256fea26469706673582212200cf1aea5547025eafdd9a16c8ed54a7ff01a00dbcc5c313365d3208df9a9ff4464736f6c634300081c0033", + "nonce": "0x1a", + "chainId": "0xaa36a7" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [ + { + "type": "0x2", + "status": "0x1", + "cumulativeGasUsed": "0x9ad954", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "transactionHash": "0x125480eac10d5ae705868169409335bd3ee5247c9988bf72d35ccbf73ba3b2fb", + "transactionIndex": "0x47", + "blockHash": "0x07fb28c4387c8933091faf833382889d1cc23e6c6824443efa2b119a3dd139d5", + "blockNumber": "0xab2168", + "gasUsed": "0x3f41f", + "effectiveGasPrice": "0x3e78bc52", + "from": "0x3f7e578d292233aa8bc811cebc357e33001c306c", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "contractAddress": "0x3fa77e57bac28940621d1aae1a136058c4064623" + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1783338727468, + "chain": 11155111, + "commit": "7ed90f5" +} \ No newline at end of file diff --git a/broadcast/DeployDepositCapStateGuard.s.sol/11155111/run-1783338776508.json b/broadcast/DeployDepositCapStateGuard.s.sol/11155111/run-1783338776508.json new file mode 100644 index 0000000..6a41293 --- /dev/null +++ b/broadcast/DeployDepositCapStateGuard.s.sol/11155111/run-1783338776508.json @@ -0,0 +1,47 @@ +{ + "transactions": [ + { + "hash": "0x125480eac10d5ae705868169409335bd3ee5247c9988bf72d35ccbf73ba3b2fb", + "transactionType": "CREATE2", + "contractName": "DepositCapStateGuard", + "contractAddress": "0x3fa77e57bac28940621d1aae1a136058c4064623", + "function": null, + "arguments": null, + "transaction": { + "from": "0x3f7e578d292233aa8bc811cebc357e33001c306c", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "gas": "0x575fc", + "value": "0x0", + "input": "0x7fde142edeac1c4c898e276a070df232a649cda0a4e930355b1f72abb019de8a608080604052346015576103b3908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c632a1620d914610024575f80fd5b3461024a57604036600319011261024a576004356001600160a01b0381169081900361024a5760243567ffffffffffffffff811161024a573660238201121561024a5780600401359067ffffffffffffffff821161024a573660248360071b8301011161024a575f5b828110156102f1578060071b82016024810160648201356084830135918282116102d85735916040519163bd85b03960e01b83528360048401526020836024818c5afa928315610269575f936102a5575b50808310801561029c575b6102745750505060405163906e277b60e01b8152816004820152610180816024818a5afa908115610269575f91610150575b50604001519160440135908183036101385750505060010161008d565b636e77284760e11b5f5260045260245260445260645ffd5b90506101803d8111610262575b61016781836102f3565b8101818103610180811261024a576101201361024a576040519060e082019082821067ffffffffffffffff83111761024e576101b5916040526101aa8185610336565b835260408401610336565b6020820152608082015160408201526101d060a08301610315565b606082015260c082015164ffffffffff8116810361024a57608082015260e08201519060ff8216820361024a5761024261016060449460409460a085015261021b6101008201610329565b60c085015261022d6101208201610315565b5061023b6101408201610315565b5001610329565b50915061011b565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b503d61015d565b6040513d5f823e3d90fd5b608494506040519363e016ae5560e01b85526004850152602484015260448301526064820152fd5b508183116100e9565b9092506020813d82116102d0575b816102c0602093836102f3565b8101031261024a5751915f6100de565b3d91506102b3565b635e4aefe960e01b5f523560045260245260445260645ffd5b005b90601f8019910116810190811067ffffffffffffffff82111761024e57604052565b51906001600160a01b038216820361024a57565b5190811515820361024a57565b919082604091031261024a576040516040810181811067ffffffffffffffff82111761024e57604052602061037881839561037081610315565b855201610329565b91015256fea26469706673582212200cf1aea5547025eafdd9a16c8ed54a7ff01a00dbcc5c313365d3208df9a9ff4464736f6c634300081c0033", + "nonce": "0x1a", + "chainId": "0xaa36a7" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [ + { + "type": "0x2", + "status": "0x1", + "cumulativeGasUsed": "0x9ad954", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "transactionHash": "0x125480eac10d5ae705868169409335bd3ee5247c9988bf72d35ccbf73ba3b2fb", + "transactionIndex": "0x47", + "blockHash": "0x07fb28c4387c8933091faf833382889d1cc23e6c6824443efa2b119a3dd139d5", + "blockNumber": "0xab2168", + "gasUsed": "0x3f41f", + "effectiveGasPrice": "0x3e78bc52", + "from": "0x3f7e578d292233aa8bc811cebc357e33001c306c", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "contractAddress": "0x3fa77e57bac28940621d1aae1a136058c4064623" + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1783338776508, + "chain": 11155111, + "commit": "7ed90f5" +} \ No newline at end of file diff --git a/broadcast/DeployDepositCapStateGuard.s.sol/11155111/run-latest.json b/broadcast/DeployDepositCapStateGuard.s.sol/11155111/run-latest.json new file mode 100644 index 0000000..6a41293 --- /dev/null +++ b/broadcast/DeployDepositCapStateGuard.s.sol/11155111/run-latest.json @@ -0,0 +1,47 @@ +{ + "transactions": [ + { + "hash": "0x125480eac10d5ae705868169409335bd3ee5247c9988bf72d35ccbf73ba3b2fb", + "transactionType": "CREATE2", + "contractName": "DepositCapStateGuard", + "contractAddress": "0x3fa77e57bac28940621d1aae1a136058c4064623", + "function": null, + "arguments": null, + "transaction": { + "from": "0x3f7e578d292233aa8bc811cebc357e33001c306c", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "gas": "0x575fc", + "value": "0x0", + "input": "0x7fde142edeac1c4c898e276a070df232a649cda0a4e930355b1f72abb019de8a608080604052346015576103b3908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c632a1620d914610024575f80fd5b3461024a57604036600319011261024a576004356001600160a01b0381169081900361024a5760243567ffffffffffffffff811161024a573660238201121561024a5780600401359067ffffffffffffffff821161024a573660248360071b8301011161024a575f5b828110156102f1578060071b82016024810160648201356084830135918282116102d85735916040519163bd85b03960e01b83528360048401526020836024818c5afa928315610269575f936102a5575b50808310801561029c575b6102745750505060405163906e277b60e01b8152816004820152610180816024818a5afa908115610269575f91610150575b50604001519160440135908183036101385750505060010161008d565b636e77284760e11b5f5260045260245260445260645ffd5b90506101803d8111610262575b61016781836102f3565b8101818103610180811261024a576101201361024a576040519060e082019082821067ffffffffffffffff83111761024e576101b5916040526101aa8185610336565b835260408401610336565b6020820152608082015160408201526101d060a08301610315565b606082015260c082015164ffffffffff8116810361024a57608082015260e08201519060ff8216820361024a5761024261016060449460409460a085015261021b6101008201610329565b60c085015261022d6101208201610315565b5061023b6101408201610315565b5001610329565b50915061011b565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b503d61015d565b6040513d5f823e3d90fd5b608494506040519363e016ae5560e01b85526004850152602484015260448301526064820152fd5b508183116100e9565b9092506020813d82116102d0575b816102c0602093836102f3565b8101031261024a5751915f6100de565b3d91506102b3565b635e4aefe960e01b5f523560045260245260445260645ffd5b005b90601f8019910116810190811067ffffffffffffffff82111761024e57604052565b51906001600160a01b038216820361024a57565b5190811515820361024a57565b919082604091031261024a576040516040810181811067ffffffffffffffff82111761024e57604052602061037881839561037081610315565b855201610329565b91015256fea26469706673582212200cf1aea5547025eafdd9a16c8ed54a7ff01a00dbcc5c313365d3208df9a9ff4464736f6c634300081c0033", + "nonce": "0x1a", + "chainId": "0xaa36a7" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [ + { + "type": "0x2", + "status": "0x1", + "cumulativeGasUsed": "0x9ad954", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "transactionHash": "0x125480eac10d5ae705868169409335bd3ee5247c9988bf72d35ccbf73ba3b2fb", + "transactionIndex": "0x47", + "blockHash": "0x07fb28c4387c8933091faf833382889d1cc23e6c6824443efa2b119a3dd139d5", + "blockNumber": "0xab2168", + "gasUsed": "0x3f41f", + "effectiveGasPrice": "0x3e78bc52", + "from": "0x3f7e578d292233aa8bc811cebc357e33001c306c", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "contractAddress": "0x3fa77e57bac28940621d1aae1a136058c4064623" + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1783338776508, + "chain": 11155111, + "commit": "7ed90f5" +} \ No newline at end of file diff --git a/script/DeployDepositCapStateGuard.s.sol b/script/DeployDepositCapStateGuard.s.sol new file mode 100644 index 0000000..dd896df --- /dev/null +++ b/script/DeployDepositCapStateGuard.s.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.22; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; + +import {DepositCapStateGuard} from "../src/checks/DepositCapStateGuard.sol"; + +/// @title DeployDepositCapStateGuard +/// @author 3F Protocol +/// @notice Deploys the stateless `DepositCapStateGuard` through the canonical CREATE2 factory +/// (`0x4e59b44847b379578588920cA78FbF26c0B4956C`, forge-std's `CREATE2_FACTORY`), so the +/// guard lands at the same address on every chain. The contract holds no state, takes no +/// constructor arguments, and needs no proxy or initialization. +/// @dev Salted `new` inside a broadcast is routed through the canonical factory by forge. The +/// resulting address is pinned in `EXPECTED_ADDRESS` and asserted after deployment: it is a +/// pure function of the init code, so it only moves if the source or the compiler profile +/// (solc 0.8.28, via-ir, 200 optimizer runs, cancun) changes — a mismatch means this script's +/// constant and the README are stale and must be recomputed. +contract DeployDepositCapStateGuard is Script { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ADDRESSES */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice CREATE2 salt, ground so the guard's address starts with `0x3fa77e57` ("3F ATTEST"). + /// @dev `cast create2 --deployer 0x4e59b44847b379578588920cA78FbF26c0B4956C --starts-with 3fa77e57 + /// --init-code-hash keccak256(creationCode)` with the profile-default compiler settings. + bytes32 internal constant SALT = 0x7fde142edeac1c4c898e276a070df232a649cda0a4e930355b1f72abb019de8a; + + /// @notice Deterministic deployment address of the guard on every chain. + address internal constant EXPECTED_ADDRESS = 0x3Fa77E57baC28940621d1aaE1a136058c4064623; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Thrown when the CREATE2 deployment does not land on `EXPECTED_ADDRESS`. + /// @param expected The pinned deterministic address. + /// @param actual The address the guard actually deployed to. + error UnexpectedGuardAddress(address expected, address actual); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* RUN */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Deploys the guard via CREATE2 and asserts the deterministic address. + function run() external { + vm.startBroadcast(); + DepositCapStateGuard guard = new DepositCapStateGuard{salt: SALT}(); + vm.stopBroadcast(); + + if (address(guard) != EXPECTED_ADDRESS) { + revert UnexpectedGuardAddress(EXPECTED_ADDRESS, address(guard)); + } + + console2.log("DepositCapStateGuard:", address(guard)); + } +} diff --git a/src/checks/DepositCapStateGuard.sol b/src/checks/DepositCapStateGuard.sol new file mode 100644 index 0000000..8ac8d6b --- /dev/null +++ b/src/checks/DepositCapStateGuard.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.22; + +import {IFacility} from "@grunt/interfaces/facility/IFacility.sol"; +import {IntentProperties} from "@grunt/libs/facility/LibIntent.sol"; + +/// @dev `Facility.totalSupply(uint256)` is a public getter on the Facility implementation but is +/// not declared on `IFacility`, so the guard declares the single view it needs here. +interface IFacilityTotalSupply { + /// @notice Returns the total supply tracked for an intent. + /// @param id The intent ID. + function totalSupply(uint256 id) external view returns (uint256); +} + +/// @title DepositCapStateGuard +/// @author 3F Protocol +/// @notice Stateless, read-only guard that asserts a Facility intent's deposit cap and total +/// supply match caller expectations, reverting otherwise. +/// @dev MUST be used as the FIRST call of a batch transaction (e.g. an EIP-7702 batch) executed +/// in the mode where any failing call reverts the whole batch. The batch calls this +/// read-only function first; if the observed on-chain state does not match the expectations +/// encoded when the batch was signed (e.g. a deposit cap changed, or deposits/withdrawals +/// moved the supply outside the tolerated range), the guard reverts and the rest of the +/// batch reverts with it. The guard holds no state and grants no permissions, so it is safe +/// to call by anyone with any arguments. +/// +/// The deposit cap is checked for exact equality, while the total supply is checked against +/// an inclusive `[minSupply, maxSupply]` range so that benign concurrent deposits or +/// withdrawals within the tolerated band do not make the batch fail. +contract DepositCapStateGuard { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STRUCTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Expected state for a single intent. + /// @param id The intent ID. + /// @param expectedCap The exact deposit cap the intent must have. + /// @param minSupply Inclusive lower bound on the intent's total supply. + /// @param maxSupply Inclusive upper bound on the intent's total supply. Use + /// `minSupply == maxSupply` to require an exact supply. + struct Check { + uint256 id; + uint256 expectedCap; + uint256 minSupply; + uint256 maxSupply; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Thrown when a check's supply bounds are inverted (`minSupply > maxSupply`). + /// @param id The intent ID. + /// @param minSupply The provided lower bound. + /// @param maxSupply The provided upper bound. + error InvalidSupplyRange(uint256 id, uint256 minSupply, uint256 maxSupply); + + /// @notice Thrown when an intent's deposit cap differs from the expected value. + /// @param id The intent ID. + /// @param expectedCap The deposit cap expected by the caller. + /// @param actualCap The deposit cap currently set on the intent. + error StaleCap(uint256 id, uint256 expectedCap, uint256 actualCap); + + /// @notice Thrown when an intent's total supply is outside the tolerated range. + /// @param id The intent ID. + /// @param minSupply Inclusive lower bound expected by the caller. + /// @param maxSupply Inclusive upper bound expected by the caller. + /// @param actualSupply The total supply currently tracked for the intent. + error SupplyOutOfRange(uint256 id, uint256 minSupply, uint256 maxSupply, uint256 actualSupply); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CHECKS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Reverts unless every intent in `checks` has exactly the expected deposit cap and a + /// total supply within the inclusive `[minSupply, maxSupply]` range. + /// @dev Call this first in a revert-on-failure batch so a revert here aborts the whole batch + /// before any state-changing call executes. Read-only: performs no writes and succeeds + /// silently when all checks pass. An empty `checks` array is a no-op. + /// @param facility The Facility to read intent state from. + /// @param checks The per-intent expectations to enforce. + function requireState(IFacility facility, Check[] calldata checks) external view { + uint256 length = checks.length; + for (uint256 i; i < length; i++) { + Check calldata c = checks[i]; + if (c.minSupply > c.maxSupply) revert InvalidSupplyRange(c.id, c.minSupply, c.maxSupply); + + uint256 supply = IFacilityTotalSupply(address(facility)).totalSupply(c.id); + if (supply < c.minSupply || supply > c.maxSupply) { + revert SupplyOutOfRange(c.id, c.minSupply, c.maxSupply, supply); + } + + (IntentProperties memory props,,,) = facility.getIntent(c.id); + if (props.depositCap != c.expectedCap) revert StaleCap(c.id, c.expectedCap, props.depositCap); + } + } +} diff --git a/test/DepositCapStateGuard.t.sol b/test/DepositCapStateGuard.t.sol new file mode 100644 index 0000000..db3e5e0 --- /dev/null +++ b/test/DepositCapStateGuard.t.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.22; + +import {Test} from "forge-std/Test.sol"; + +import {IFacility} from "@grunt/interfaces/facility/IFacility.sol"; +import {IntentProperties} from "@grunt/libs/facility/LibIntent.sol"; + +import {DepositCapStateGuard} from "src/checks/DepositCapStateGuard.sol"; + +import {MockFacility} from "./MorphoAllocator.t.sol"; + +/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ +/* TESTS */ +/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + +contract DepositCapStateGuardTest is Test { + DepositCapStateGuard internal guard; + MockFacility internal facility; + + uint256 internal constant INTENT_ID = 7; + uint256 internal constant CAP = 1_000_000e6; + uint256 internal constant SUPPLY = 400_000e6; + + function setUp() public { + facility = new MockFacility(); + guard = new DepositCapStateGuard(); + + _setIntent(INTENT_ID, CAP, SUPPLY); + } + + /*========== helpers ==========*/ + + function _setIntent(uint256 id, uint256 cap, uint256 supply) internal { + IntentProperties memory props; + props.depositCap = cap; + facility.setIntent(id, props); + facility.setTotalSupply(id, supply); + } + + function _check(uint256 id, uint256 expectedCap, uint256 minSupply, uint256 maxSupply) + internal + pure + returns (DepositCapStateGuard.Check memory) + { + return DepositCapStateGuard.Check({id: id, expectedCap: expectedCap, minSupply: minSupply, maxSupply: maxSupply}); + } + + function _requireState(DepositCapStateGuard.Check[] memory checks) internal view { + guard.requireState(IFacility(address(facility)), checks); + } + + /*========== passing checks ==========*/ + + function test_requireState_passesWithinRange() public view { + DepositCapStateGuard.Check[] memory checks = new DepositCapStateGuard.Check[](1); + checks[0] = _check(INTENT_ID, CAP, SUPPLY - 100e6, SUPPLY + 100e6); + _requireState(checks); + } + + function test_requireState_passesOnExactBounds() public view { + // minSupply == maxSupply == supply degenerates to an exact-supply check. + DepositCapStateGuard.Check[] memory checks = new DepositCapStateGuard.Check[](1); + checks[0] = _check(INTENT_ID, CAP, SUPPLY, SUPPLY); + _requireState(checks); + } + + function test_requireState_passesOnInclusiveEdges() public { + DepositCapStateGuard.Check[] memory checks = new DepositCapStateGuard.Check[](1); + + facility.setTotalSupply(INTENT_ID, SUPPLY); + checks[0] = _check(INTENT_ID, CAP, SUPPLY, SUPPLY + 1); + _requireState(checks); + + checks[0] = _check(INTENT_ID, CAP, SUPPLY - 1, SUPPLY); + _requireState(checks); + } + + function test_requireState_passesOnEmptyChecks() public view { + _requireState(new DepositCapStateGuard.Check[](0)); + } + + function test_requireState_passesOnMultipleIntents() public { + _setIntent(8, CAP * 2, SUPPLY * 2); + + DepositCapStateGuard.Check[] memory checks = new DepositCapStateGuard.Check[](2); + checks[0] = _check(INTENT_ID, CAP, 0, SUPPLY); + checks[1] = _check(8, CAP * 2, SUPPLY, type(uint256).max); + _requireState(checks); + } + + /*========== reverting checks ==========*/ + + function test_requireState_revertsWhenSupplyBelowRange() public { + DepositCapStateGuard.Check[] memory checks = new DepositCapStateGuard.Check[](1); + checks[0] = _check(INTENT_ID, CAP, SUPPLY + 1, SUPPLY + 100e6); + + vm.expectRevert( + abi.encodeWithSelector( + DepositCapStateGuard.SupplyOutOfRange.selector, INTENT_ID, SUPPLY + 1, SUPPLY + 100e6, SUPPLY + ) + ); + _requireState(checks); + } + + function test_requireState_revertsWhenSupplyAboveRange() public { + DepositCapStateGuard.Check[] memory checks = new DepositCapStateGuard.Check[](1); + checks[0] = _check(INTENT_ID, CAP, 0, SUPPLY - 1); + + vm.expectRevert( + abi.encodeWithSelector(DepositCapStateGuard.SupplyOutOfRange.selector, INTENT_ID, 0, SUPPLY - 1, SUPPLY) + ); + _requireState(checks); + } + + function test_requireState_revertsOnStaleCap() public { + DepositCapStateGuard.Check[] memory checks = new DepositCapStateGuard.Check[](1); + checks[0] = _check(INTENT_ID, CAP + 1, 0, type(uint256).max); + + vm.expectRevert(abi.encodeWithSelector(DepositCapStateGuard.StaleCap.selector, INTENT_ID, CAP + 1, CAP)); + _requireState(checks); + } + + function test_requireState_revertsOnInvertedRange() public { + DepositCapStateGuard.Check[] memory checks = new DepositCapStateGuard.Check[](1); + checks[0] = _check(INTENT_ID, CAP, SUPPLY + 1, SUPPLY - 1); + + vm.expectRevert( + abi.encodeWithSelector(DepositCapStateGuard.InvalidSupplyRange.selector, INTENT_ID, SUPPLY + 1, SUPPLY - 1) + ); + _requireState(checks); + } + + function test_requireState_revertsWhenAnyCheckFails() public { + // First intent passes; the second intent's stale cap must still revert the whole call. + _setIntent(8, CAP * 2, SUPPLY * 2); + + DepositCapStateGuard.Check[] memory checks = new DepositCapStateGuard.Check[](2); + checks[0] = _check(INTENT_ID, CAP, 0, type(uint256).max); + checks[1] = _check(8, CAP, 0, type(uint256).max); + + vm.expectRevert(abi.encodeWithSelector(DepositCapStateGuard.StaleCap.selector, 8, CAP, CAP * 2)); + _requireState(checks); + } + + /*========== fuzz ==========*/ + + function testFuzz_requireState_supplyRange(uint256 supply, uint256 minSupply, uint256 maxSupply) public { + vm.assume(minSupply <= maxSupply); + facility.setTotalSupply(INTENT_ID, supply); + + DepositCapStateGuard.Check[] memory checks = new DepositCapStateGuard.Check[](1); + checks[0] = _check(INTENT_ID, CAP, minSupply, maxSupply); + + if (supply < minSupply || supply > maxSupply) { + vm.expectRevert( + abi.encodeWithSelector(DepositCapStateGuard.SupplyOutOfRange.selector, INTENT_ID, minSupply, maxSupply, supply) + ); + } + _requireState(checks); + } +} diff --git a/test/MorphoAllocator.t.sol b/test/MorphoAllocator.t.sol index 23fc7f5..91667d5 100644 --- a/test/MorphoAllocator.t.sol +++ b/test/MorphoAllocator.t.sol @@ -53,6 +53,7 @@ contract MockFacility { mapping(uint256 => address[]) internal _tokens; mapping(uint256 => mapping(address => bool)) internal _tokenSeenByIntent; mapping(uint256 => Order) internal _orders; + mapping(uint256 => uint256) internal _totalSupply; // Unlock knob: amount of `unlockToken` to credit to the intent on `unlock`. uint256 public unlockMintAmount; @@ -89,6 +90,10 @@ contract MockFacility { _props[id] = props; } + function setTotalSupply(uint256 id, uint256 supply) external { + _totalSupply[id] = supply; + } + function setIntentBalance(uint256 id, address token, uint256 amount) external { if (!_tokenSeenByIntent[id][token]) { _tokenSeenByIntent[id][token] = true; @@ -188,6 +193,10 @@ contract MockFacility { return (_orders[id], bytes32(0)); } + function totalSupply(uint256 id) external view returns (uint256) { + return _totalSupply[id]; + } + /// @notice Stand-in for `IFund.state(Order)` — the allocator queries this after commit/unlock. function state(Order calldata) external view returns (State) { return orderState;