diff --git a/.gas-snapshot b/.gas-snapshot
index 1c76341..0379d21 100644
--- a/.gas-snapshot
+++ b/.gas-snapshot
@@ -1,51 +1,62 @@
-HatsSignerGateFactoryTest:testCannotReinitializeHSGSingleton() (gas: 27411)
-HatsSignerGateFactoryTest:testCannotReinitializeMHSGSingleton() (gas: 27912)
-HatsSignerGateFactoryTest:testDeployFactory() (gas: 29958)
-HatsSignerGateFactoryTest:testDeployHatsSignerGate() (gas: 709270)
-HatsSignerGateFactoryTest:testDeployHatsSignersGateAndSafe() (gas: 750575)
-HatsSignerGateFactoryTest:testDeployHatsSignersGateAndSafe(uint256,uint256,uint256,uint256,uint256) (runs: 256, μ: 762196, ~: 766705)
-HatsSignerGateFactoryTest:testDeployMultiHatsSignerGate() (gas: 694540)
-HatsSignerGateTest:testAddSingleSigner() (gas: 117395)
-HatsSignerGateTest:testAddThreeSigners() (gas: 223310)
-HatsSignerGateTest:testAddTooManySigners() (gas: 338958)
-HatsSignerGateTest:testAttackOnMaxSigner2Fails() (gas: 441838)
-HatsSignerGateTest:testAttackOnMaxSignerFails() (gas: 357641)
-HatsSignerGateTest:testCanRemoveInvalidSigner1() (gas: 105991)
-HatsSignerGateTest:testCanRemoveInvalidSignerAfterReconcile2Signers() (gas: 178796)
-HatsSignerGateTest:testCanRemoveInvalidSignerAfterReconcile3PLusSigners() (gas: 231639)
-HatsSignerGateTest:testCanRemoveInvalidSignerWhenMultipleSigners() (gas: 165389)
-HatsSignerGateTest:testCannotAddNewModules() (gas: 501974)
-HatsSignerGateTest:testCannotCallCheckAfterExecutionFromNonSafe() (gas: 13286)
-HatsSignerGateTest:testCannotCallCheckTransactionFromNonSafe() (gas: 15101)
-HatsSignerGateTest:testCannotDecreaseThreshold() (gas: 519965)
-HatsSignerGateTest:testCannotDisableGuard() (gas: 470954)
-HatsSignerGateTest:testCannotDisableModule() (gas: 480731)
-HatsSignerGateTest:testCannotIncreaseThreshold() (gas: 519964)
-HatsSignerGateTest:testCannotRemoveValidSigner() (gas: 122351)
-HatsSignerGateTest:testClaimSigner() (gas: 116134)
-HatsSignerGateTest:testExecByLessThanMinThresholdReverts() (gas: 344897)
-HatsSignerGateTest:testExecTxByHatWearers() (gas: 640792)
-HatsSignerGateTest:testExecTxByNonHatWearersReverts() (gas: 601538)
-HatsSignerGateTest:testExecTxByTooFewOwnersReverts() (gas: 278945)
-HatsSignerGateTest:testNonHatWearerCannotClaimSigner() (gas: 41678)
-HatsSignerGateTest:testNonOwnerCannotSetMinThreshold() (gas: 24831)
-HatsSignerGateTest:testNonOwnerHatWearerCannotSetTargetThreshold() (gas: 37023)
-HatsSignerGateTest:testOwnerClaimSignerReverts() (gas: 165961)
-HatsSignerGateTest:testReconcileSignerCount() (gas: 197753)
-HatsSignerGateTest:testSetInvalidMinThreshold() (gas: 23454)
-HatsSignerGateTest:testSetMinThreshold() (gas: 39537)
-HatsSignerGateTest:testSetTargetThreshold() (gas: 126335)
-HatsSignerGateTest:testSetTargetThreshold3of4() (gas: 277217)
-HatsSignerGateTest:testSetTargetThreshold4of4() (gas: 277202)
-HatsSignerGateTest:testTargetSigAttackFails() (gas: 637029)
-HatsSignerGateTest:testValidSignersCanClaimAfterMaxSignerLosesHat() (gas: 340754)
-MultiHatsSignerGateTest:test_Multi_AddSingleSigner() (gas: 142836)
-MultiHatsSignerGateTest:test_Multi_AddTwoSigners_DifferentHats() (gas: 228296)
-MultiHatsSignerGateTest:test_Multi_CanRemoveInvalidSigner1() (gas: 128812)
-MultiHatsSignerGateTest:test_Multi_CannotRemoveValidSigner() (gas: 150843)
-MultiHatsSignerGateTest:test_Multi_ExecTxByHatWearers() (gas: 725110)
-MultiHatsSignerGateTest:test_Multi_ExecTxByNonHatWearersReverts() (gas: 686281)
-MultiHatsSignerGateTest:test_Multi_NonHatWearerCannotClaimSigner(uint256) (runs: 256, μ: 44655, ~: 44655)
-MultiHatsSignerGateTest:test_Multi_NonOwnerCannotAddSignerHats() (gas: 23797)
-MultiHatsSignerGateTest:test_Multi_OwnerCanAddSignerHats(uint256) (runs: 256, μ: 134583, ~: 73557)
-MultiHatsSignerGateTest:test_Multi_OwnerCanAddSignerHats1() (gas: 50288)
\ No newline at end of file
+HatsSignerGateFactoryTest:testCanAttachHSGToSafeReturnsFalseWithModule() (gas: 705917)
+HatsSignerGateFactoryTest:testCanAttachHSGToSafeReturnsFalseWithUnsafeSignerCounts() (gas: 772217)
+HatsSignerGateFactoryTest:testCanAttachHSGToSafeReturnsTrue() (gas: 660642)
+HatsSignerGateFactoryTest:testCannotDeployHSGToSafeWithExistingModules() (gas: 416795)
+HatsSignerGateFactoryTest:testCannotReinitializeHSGSingleton() (gas: 27996)
+HatsSignerGateFactoryTest:testDeployFactory() (gas: 27002)
+HatsSignerGateFactoryTest:testDeployHatsSignerGate() (gas: 685309)
+HatsSignerGateFactoryTest:testDeployHatsSignersGateAndSafe() (gas: 739109)
+HatsSignerGateTest:testAddSingleSigner() (gas: 124739)
+HatsSignerGateTest:testAddThreeSigners() (gas: 270909)
+HatsSignerGateTest:testAddTooManySigners() (gas: 438764)
+HatsSignerGateTest:testAttackOnMaxSigner2Fails() (gas: 639001)
+HatsSignerGateTest:testAttackOnMaxSignerFails() (gas: 496066)
+HatsSignerGateTest:testAttackerCannotExploitSigHandlingDifferences() (gas: 746840)
+HatsSignerGateTest:testCanClaimToReplaceInvalidSignerAtMaxSigner() (gas: 449376)
+HatsSignerGateTest:testCanRemoveInvalidSigner1() (gas: 117157)
+HatsSignerGateTest:testCanRemoveInvalidSignerAfterReconcile2Signers() (gas: 208130)
+HatsSignerGateTest:testCanRemoveInvalidSignerAfterReconcile3PLusSigners() (gas: 284517)
+HatsSignerGateTest:testCanRemoveInvalidSignerWhenMultipleSigners() (gas: 189512)
+HatsSignerGateTest:testCannotCallCheckAfterExecutionFromNonSafe() (gas: 13307)
+HatsSignerGateTest:testCannotCallCheckTransactionFromNonSafe() (gas: 15066)
+HatsSignerGateTest:testCannotClaimSignerIfNoInvalidSigners() (gas: 448351)
+HatsSignerGateTest:testCannotDecreaseThreshold() (gas: 588873)
+HatsSignerGateTest:testCannotDisableGuard() (gas: 514194)
+HatsSignerGateTest:testCannotDisableModule() (gas: 532910)
+HatsSignerGateTest:testCannotIncreaseThreshold() (gas: 588848)
+HatsSignerGateTest:testCannotRemoveValidSigner() (gas: 126447)
+HatsSignerGateTest:testClaimSigner() (gas: 116709)
+HatsSignerGateTest:testExecByLessThanMinThresholdReverts() (gas: 365575)
+HatsSignerGateTest:testExecTxByHatWearers() (gas: 710354)
+HatsSignerGateTest:testExecTxByNonHatWearersReverts() (gas: 635066)
+HatsSignerGateTest:testExecTxByTooFewOwnersReverts() (gas: 280263)
+HatsSignerGateTest:testNonHatWearerCannotClaimSigner() (gas: 51436)
+HatsSignerGateTest:testNonOwnerCannotSetMinThreshold() (gas: 24810)
+HatsSignerGateTest:testNonOwnerHatWearerCannotSetTargetThreshold() (gas: 37068)
+HatsSignerGateTest:testOwnerClaimSignerReverts() (gas: 196834)
+HatsSignerGateTest:testRemoveSignerCorrectlyUpdates() (gas: 406391)
+HatsSignerGateTest:testSetInvalidMinThreshold() (gas: 23498)
+HatsSignerGateTest:testSetMinThreshold() (gas: 58199)
+HatsSignerGateTest:testSetTargetThreshold() (gas: 132499)
+HatsSignerGateTest:testSetTargetThreshold3of4() (gas: 348934)
+HatsSignerGateTest:testSetTargetThreshold4of4() (gas: 348917)
+HatsSignerGateTest:testSetTargetThresholdUpdatesThresholdCorrectly() (gas: 429651)
+HatsSignerGateTest:testSetTargetTresholdCannotSetBelowMinThreshold() (gas: 25621)
+HatsSignerGateTest:testSignersCannotAddNewModules() (gas: 558228)
+HatsSignerGateTest:testSignersCannotAddOwners() (gas: 624944)
+HatsSignerGateTest:testSignersCannotReenterCheckTransactionToAddOwners() (gas: 660279)
+HatsSignerGateTest:testSignersCannotRemoveOwners() (gas: 597613)
+HatsSignerGateTest:testSignersCannotSwapOwners() (gas: 627440)
+HatsSignerGateTest:testTargetSigAttackFails() (gas: 732130)
+HatsSignerGateTest:testValidSignersCanClaimAfterLastMaxSignerLosesHat() (gas: 458272)
+HatsSignerGateTest:testValidSignersCanClaimAfterMaxSignerLosesHat() (gas: 448577)
+HatsSignerGateTest:test_Multi_AddSingleSigner() (gas: 129793)
+HatsSignerGateTest:test_Multi_AddTwoSigners_DifferentHats() (gas: 211275)
+HatsSignerGateTest:test_Multi_CanRemoveInvalidSigner1() (gas: 130354)
+HatsSignerGateTest:test_Multi_CannotRemoveValidSigner() (gas: 139623)
+HatsSignerGateTest:test_Multi_ExecTxByHatWearers() (gas: 725864)
+HatsSignerGateTest:test_Multi_ExecTxByNonHatWearersReverts() (gas: 652771)
+HatsSignerGateTest:test_Multi_NonHatWearerCannotClaimSigner(uint256) (runs: 256, μ: 54284, ~: 54284)
+HatsSignerGateTest:test_Multi_NonOwnerCannotAddSignerHats() (gas: 23776)
+HatsSignerGateTest:test_Multi_OwnerCanAddSignerHats(uint256) (runs: 256, μ: 250710, ~: 73535)
+HatsSignerGateTest:test_Multi_OwnerCanAddSignerHats1() (gas: 50332)
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c11b721..4c107bb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,6 +6,12 @@ on:
- main
pull_request:
+env:
+ FOUNDRY_PROFILE: ci
+ INFURA_KEY: ${{ secrets.INFURA_KEY }}
+ GC_RPC: ${{ secrets.GC_RPC }}
+ PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
+
jobs:
lint:
name: "Markdown linting"
@@ -29,6 +35,13 @@ jobs:
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
+
+ # removed until transient keyword is supported
+ # - name: Check formatting
+ # run: forge fmt --check
+
+ - name: Check contract sizes
+ run: forge build --sizes --skip script --skip test --via-ir
- name: Run tests
run: forge test -vvv
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index cd89fa3..1745dcd 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,21 +1,25 @@
-[submodule "lib/forge-std"]
- path = lib/forge-std
- url = https://github.com/foundry-rs/forge-std
-[submodule "lib/zodiac"]
- path = lib/zodiac
- url = https://github.com/gnosis/zodiac
-[submodule "lib/hats-auth"]
- path = lib/hats-auth
- url = https://github.com/Hats-Protocol/hats-auth
-[submodule "lib/safe-contracts"]
- path = lib/safe-contracts
- url = https://github.com/Hats-Protocol/safe-contracts
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
- url = https://github.com/Openzeppelin/openzeppelin-contracts
+ url = https://github.com/openzeppelin/openzeppelin-contracts
[submodule "lib/openzeppelin-contracts-upgradeable"]
path = lib/openzeppelin-contracts-upgradeable
- url = https://github.com/Openzeppelin/openzeppelin-contracts-upgradeable
+ url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable
[submodule "lib/hats-protocol"]
path = lib/hats-protocol
url = https://github.com/hats-protocol/hats-protocol
+[submodule "lib/safe-smart-account"]
+ path = lib/safe-smart-account
+ url = https://github.com/safe-global/safe-smart-account
+[submodule "lib/zodiac"]
+ path = lib/zodiac
+ url = https://github.com/gnosisguild/zodiac
+
+[submodule "lib/forge-std"]
+ path = lib/forge-std
+ url = https://github.com/foundry-rs/forge-std
+[submodule "lib/openzeppelin-contracts"]
+ path = lib/openzeppelin-contracts
+ url = https://github.com/openzeppelin/openzeppelin-contracts
+[submodule "lib/solady"]
+ path = lib/solady
+ url = https://github.com/vectorized/solady
diff --git a/LICENSE b/LICENSE
index 70050a3..b632132 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,165 @@
-MIT License
-
-Copyright (c) 2023 Haberdasher Labs
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+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.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser 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
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/README.md b/README.md
index 1aed83c..04174ff 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,10 @@
-# hats-zodiac
+# Hats Signer Gate
-This repo holds several [Hats Protocol](https://github.com/Hats-Protocol/hats-protocol)-enabled [Zodiac](https://github.com/gnosis/zodiac) contracts. Currently, this repo contains the following, referred to collectively as Hats Signer Gate (HSG):
+This repo holds a [Hats Protocol](https://github.com/Hats-Protocol/hats-protocol)-enabled [Zodiac](https://github.com/gnosis/zodiac) contract called Hats Signer Gate (HSG).
-- [Hats Signer Gate](#hats-signer-gate)
-- [Multi-Hats Signer Gate](#multi-hats-signer-gate)
-- [Hats Signer Gate Factory](#hats-signer-gate-factory)
+## Hats Signer Gate v2
-## Hats Signer Gate
-
-A contract that grants multisig signing rights to addresses wearing a given Hat, enabling on-chain organizations (such as DAOs) to revocably delegate constrained signing authority and responsibility to individuals.
+A contract that grants multisig signing rights to addresses wearing a given hats, enabling on-chain organizations (such as DAOs) to revocably delegate to individuals constrained authority and responsibility to operate an account (i.e. a Safe) owned by the organization.
### Overview
@@ -29,43 +25,139 @@ A) **Only valid signers can execute transactions**, i.e. only signatures made by
B) **Signers cannot execute transactions that remove the constraint in (A)**. Specifically, this contract guards against signers...
1. Removing the contract as a guard on the multisig
-2. Removing the contract as a module on the multisig — or removing/changing/adding any other modules,
+2. Removing the contract as a module on the multisig — or removing/changing/adding any other modules
3. Changing the multisig threshold
4. Changing the multisig owners
+5. Making delegatecalls to any target not approved by the owner
> **Warning**
> Protections against (3) and (4) above only hold if the Safe does not have any authority over the signer Hat(s). If it does — e.g. it wears an admin Hat of the signer Hat(s) or is an eligibility or toggle module on the signer Hat(s) — then in some cases the signers may be able to change the multisig threshold or owners.
>
> Proceed with caution if granting such authority to a Safe attached to HatsSignerGate.
-### Contract Ownership
+### Signer Management
+
+Hats Signer Gate provides several ways to manage Safe signers based on their hat-wearing status:
+
+#### Claiming Signer Rights
+
+- Individual hat wearers can claim their own signing rights via `claimSigner()`
+- Must be wearing a valid signer hat at time of claim
+- Each signer's hat ID is registered and tracked for future validation
+
+#### Claiming for Others
+
+When enabled by the owner (`claimableFor = true`):
+
+- Anyone can claim signing rights on behalf of valid hat wearers via `claimSignerFor()` or `claimSignersFor()`
+- Useful for batch onboarding of signers
+- Prevents re-registration if signer is still wearing their currently registered hat
+
+#### Signer Removal
+
+- Signers who no longer wear their registered hat can be removed via `removeSigner()`
+- Threshold automatically adjusts according to the threshold configuration
+- If the removed signer was the last valid signer, the contract itself becomes the sole owner
+
+### Threshold Configuration
+
+The threshold (number of required signatures) is managed dynamically based on the `ThresholdConfig`:
+
+#### Threshold Types
+
+1. **ABSOLUTE**
+
+ - Sets a fixed target number of required signatures
+ - Example: Always require exactly 3 signatures
+ - Bounded by min threshold and number of valid signers
+
+2. **PROPORTIONAL**
+
+ - Sets a percentage of total signers required (in basis points)
+ - Example: Require 51% of signers (5100 basis points)
+ - Actual number of required signatures rounds up
+ - Still bounded by min threshold
+
+#### Configuration Parameters
+
+- `min`: Minimum number of required signatures (must be > 0)
+- `target`: Either fixed number (ABSOLUTE) or percentage in basis points (PROPORTIONAL)
+- `thresholdType`: ABSOLUTE (0) or PROPORTIONAL (1)
-Hats Signer Gate uses the [HatsOwned](https://github.com/Hats-Protocol/hats-auth/) mix-in to manage ownership via a specified `ownerHat`.
+The Safe's threshold is automatically adjusted when:
+
+- New signers are added
+- Existing signers are removed
+- Threshold configuration is changed
+
+### Delegatecall Targets
+
+HSG restricts delegatecalls to protect the Safe from unauthorized modifications. Only approved targets can receive delegatecalls.
+
+#### Default Enabled Targets
+
+The following MultiSend libraries are enabled by default:
+
+| Address | Version | Type |
+| --- | --- | --- |
+| `0x40A2aCCbd92BCA938b02010E17A5b8929b49130D` | v1.3.0 | canonical |
+| `0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B` | v1.3.0 | eip155 |
+| `0x9641d764fc13c8B624c04430C7356C1C7C8102e2` | v1.4.1 | canonical |
+
+See [safe-deployments](https://github.com/safe-global/safe-deployments/tree/main/src/assets) for more information.
+
+#### Security Considerations
+
+- Delegatecalls can modify Safe state if not properly restricted. Owners should NOT approve delegatecall targets that enable the following:
+ - Directly modifying any of the Safe's state, including the Safe's nonce.
+ - Additional delegatecalls. For example, the [MultiSend.sol](https://github.com/safe-global/safe-smart-account/blob/v1.4.1-3/contracts/libraries/MultiSend.sol) library that is *not* "call only" should not be approved. The [MultiSendCallOnly.sol](https://github.com/safe-global/safe-smart-account/blob/v1.4.1-3/contracts/libraries/MultiSendCallOnly.sol) is approved by default.
+- HSG validates that approved delegatecalls don't modify critical Safe parameters, but relies on the Safe' nonce to do so.
+- Direct calls to the Safe are always prohibited
+- When detaching HSG from a Safe — i.e. when calling `detach()` — the owner must trust that admin(s) of the signer Hat(s) will not front-run the detachment to add arbitrary signers. Since admins in Hats Protocol are already trusted (and can be revoked, held accountable, etc.) this is not an additional risk, but HSG owners should nonetheless be aware of this risk.
+
+### Contract Ownership
The wearer of the `ownerHat` can make the following changes to Hats Signer Gate:
1. "Transfer" ownership to a new Hat by changing the `ownerHat`
-2. Set the acceptable multisig threshold range by changing `minThreshold` and `targetThreshold`
-3. Add other Zodiac modules to the multisig
-4. In [Multi-Hats Signer Gate](#multi-hats-signer-gate), add other Hats as valid signer Hats
-
-### Multi-Hats Signer Gate
+2. Change the threshold configuration
+3. Enable other Zodiac modules on HSG itself
+4. Enable another Zodiac guard on HSG itself
+5. Add other Hats as valid signer Hats
+6. Enable or disable the ability for others to claim signer rights on behalf of valid hat wearers (`claimableFor`)
+7. Detach HatsSignerGate from the Safe (removing it as both guard and module)
+8. Migrate to a new HatsSignerGate instance
+9. Enable or disable specific delegatecall targets
+10. Lock the contract permanently, preventing any further owner changes
-[MultiHatsSignerGate.sol](./src/MultiHatsSignerGate.sol) is a modification of Hats Signer Gate that supports setting multiple Hats as valid signer Hats.
+### Deploying New Instances
-### Hats Signer Gate Factory
+Instances of HSG can be created via the [Zodiac module proxy factory](https://github.com/gnosisguild/zodiac/blob/18b7575bb342424537883f7ebe0a94cd7f3ec4f6/contracts/factory/ModuleProxyFactory.sol).
-[HatsSignerGateFactory](./src/HatsSignerGateFactory.sol) is a factory contract that enables users to deploy proxy instances of HatsSignerGate and MultiHatsSignerGate, either for an existing Safe or wired up to a new Safe deployed at the same time. It uses the [Zodiac module proxy factory](https://github.com/gnosis/zodiac/blob/master/contracts/factory/ModuleProxyFactory.sol) so that the deployments are tracked in the Zodiac subgraph.
+Instances can be created for an existing Safe by passing the Safe address on initialization, or for a new Safe to be deployed from within HSG's initialization.
### Security Audits
-This project has received the following security audits. See the [audits directory](./audits/) for the detailed reports.
+#### v1
+
+v1 of this project has received the following security audits. See the [v1 audits directory](./docs/audit-v1/) for the detailed reports.
| Auditor | Report Date | Commit Hash | Notes |
| --- | --- | --- | --- |
| Trust Security | Feb 23, 2023 | [b9b7fcf](https://github.com/Hats-Protocol/hats-zodiac/commit/b9b7fcf22fd5cbb98c7d93dead590e80bf9c780a) | Report also includes findings from [Hats Protocol](https://github.com/Hats-Protocol/hats-protocol) audit |
| Sherlock | May 3, 2023 | [9455c0](https://github.com/Hats-Protocol/hats-zodiac/commit/9455cc0957762f5dbbd8e62063d970199109b977) | Report also includes findings from [Hats Protocol](https://github.com/Hats-Protocol/hats-protocol) audit |
+#### v2
+
+v2 — the present version — has received the following security audits. See the [v2 audits directory](./docs/audit-v2/) for the detailed reports.
+
+| Auditor | Report Date | Commit Hash | Notes |
+| --- | --- | --- | --- |
+| Sherlock | December 13, 2024 | [a9e3f4f](https://github.com/Hats-Protocol/hats-zodiac/commit/a9e3f4f0e968fb332800a468eddcb993fc6d5cd2) | 166 auditors participated |
+
+> **Note**
+> Since this audit was completed, HSG code was updated to add a variable salt to the Safe proxy creation within the `SafeManagerLib.deploySafeAndAttachHSG` function. This ensures that the address of the Safe proxy is unique to the HSG instance.
+
### Recent Deployments
See [Releases](https://github.com/Hats-Protocol/hats-zodiac/releases) for deployments. Specific deployment parameters are [stored here](./script/DeployParams.json).
diff --git a/docs/AUDITING.md b/docs/AUDITING.md
new file mode 100644
index 0000000..8ea0f70
--- /dev/null
+++ b/docs/AUDITING.md
@@ -0,0 +1,200 @@
+# HatsSignerGate v2 for auditors
+
+## Introduction: Hats Protocol
+
+[Hats Protocol](https://github.com/Hats-Protocol/hats-protocol) is a protocol for creating and managing onchain roles, aka “hats.” Organizations typically use hats to represent tasks, jobs, workstreams, teams, departments, etc.
+
+Hats Protocol uses a singleton (multitenant) architecture. All organizations' roles are stored in the same contract. Each organization has full control over its own roles, and no control over the roles of another organization unless explicitly granted.
+
+Hats are onchain objects, exposed to other contracts and offchain consumers as ERC1155 tokens. The id of a given hat is its token id.
+
+Within a given organization, hats form in a tree structure, with parent-child relationships. With the exception of the root of the tree (the “top hat”), hats are not transferable by the owner of the hat token (the “wearer” of the hat).
+
+Every hat has several key properties:
+
+- An admin (its parent hat, a uint), which can create new child hats, modify the properties of its children, and mint its children to accounts (“wearers”). Admins can exercise these powers transitively for all children, children’s children, etc.
+- An eligibility module (address), which can a) determine which accounts are allowed to wear the hat, and b) revoke the hat from existing wearers. Eligibility modules can push such status changes to the protocol, or they can implement the IHatsEligibility interface to enable the protocol to pull wearer status from their own state.
+- A toggle module (address), which can deactivate the hat. Toggle modules can push such status changes to the protocol, or they can implement the IHatsToggle interface to enable the protocol to pull hat status from their own state.
+A max supply (uint32), which is the maximum number of accounts that can wear the hat.
+- Mutability (bool), which determines whether or not the admin can change any of these properties. If a hat is immutable, its admins can mint it to new wearers, the eligibility module can revoke it from current wearers, and the toggle module can deactivate it, but nothing else can change.
+- A details/metadata field (string)
+- An image uri field (string)
+
+Hats can be revoked in two ways:
+
+1. “Statically,” resulting in the wearer’s hat token being fully burned (burn event emitted). This occurs when an eligibility module pushes an update to the protocol, as described above.
+2. “Dynamically,” resulting in the wearer’s balance of the hat going to 0 but without a full burn of the token (no burn event emitted). This occurs when the protocol pulls either a) “ineligible” status from a compliant eligibility module, or b) “inactive” status from a compliant toggle module.
+ - Dynamic revocation is made possible by staticcalls (the “pulls”) out to the hat’s eligibility and toggle modules from within the ERC1155.balanceOf function.
+ - If a hat has been dynamically revoked, any account can poke the protocol to fully burn the token (burn event emitted), but this is not required for the revocation to count. The source of truth is always the balance returned by ERC1155.balanceOf.
+
+A given ethereum account can wear many hats, but the protocol enforces that a given account cannot have more than 1 copy of a given hat (ie no account’s balance of a given hat must always be 0 or 1).
+
+## Attaching Permissions to Hats
+
+One of the foundational use cases of hats is attaching permissions. Much like in role-based access control systems, multiple permissions can be attached to hats to make permission management easier and more efficient.
+
+Attaching permissions to hats works via token gating (this is one reason Hats implements the ERC1155 interface), and can include both offchain permissions (via standard token-gating techniques leveraging Sign In With Ethereum) and onchain permissions. Onchain permissioning via hats looks a lot like standard address-based permissioning in smart contracts functions (such as in OZ.Ownable, OZ.AccessControl, etc), but instead of checking whether msg.sender is authorized within the same contract, a hat-permission function checks whether msg.sender has a balance of the specific hat token. Note that this means that authorization can change without needing to interact with the target contract.
+
+## HatsSignerGate
+
+HatsSignerGate (HSG) is an adapter that enables organizations to attach Safe multisig signing permissions to hats. Wearers of a specified hat can become signers on an HSG-gated Safe, and only wearers of the specified hat can provide valid signatures when attempting to execute a multisig transaction. HSG accomplishes the former as a module enabled on the Safe, and the latter as a guard set on the Safe.
+
+It is designed to enable organizations to delegate operations of a Safe and the assets therein to a set of operators (“signers”) while retaining ultimate ownership of the Safe account, including who the signers are.
+Security
+From a security perspective, the ideal way to build such a product would be to fork the Safe contract and insert out hat-based signer management logic natively (or build an entirely new contract). Since Safe was originally designed to be controlled by a sovereign set of owners rather than a delegated set of operators, the signers on a Safe have many ways to control the Safe’s properties. In order to ensure that the Safe remains under the control of the delegating organization, there is a lot of surface area to cover.
+
+However, the Safe ecosystem has such strong network effects that it is worth the extra effort. The organizations that use Hats Protocol specifically want delegated Safes, not some other type of account. And they want to use the Safe-compatible apps they are already familiar with, like Safe’s own UI or others like Den.
+
+Therefore, the primary objective HatsSignerGate seeks to accomplish is to lock down a Safe so that its signers can use the Safe to manage its assets but not change any of its properties. Of utmost importance is that the signers not be able to jailbreak those constraints. Specifically, the signers should not be able to:
+
+- Add or remove signers
+- Change the threshold
+- Disable HSG as a module on the Safe
+- Remove HSG as a guard on the Safe
+- Do anything else that would allow them to do 1-4. This includes:
+ - Enabling other modules on the Safe
+ - Executing delegatecalls to contracts that directly update the Safe's state in such a way that cannot be detected by HSG's guard functionality
+ - Changing the Safe singleton that provides the logic to the Safe's proxy
+ - Changing the fallback handler
+
+> **Warning**
+> These limitations must not be violated.
+
+## Tradeoffs and Limitations of v1
+
+The contract that you are currently auditing is HSG v2. HSG v1 was developed and audited ~18 months ago. Since then, we have learned a lot about how organizations want to use it, and our understanding of the full surface area of the Safe contract has advanced. As a result, a number of limitations and sub-optimalities with v1 have surfaced which have driven us to develop v2.
+
+### A) Incompatibility with other Safe modules and guards
+
+Perhaps the biggest thing we learned from our users is that they want to use HSG with additional modules and guards such as UMA’s oSnap module, Decent’s Fractal module, OZ’s timelock guard, Gnosis Guild’s Roles Mod, Connext’s Crosschain module, etc.
+
+But in v1 we explicitly disabled additional modules because modules have full control over the Safe. If the signers were allowed to add a module of their choosing, they would have unmediated control over the Safe. They could change anything, including detaching HSG, which is exactly what we want to protect against.
+
+### B) Delegatecalls
+
+One of the most difficult elements of Safe to handle correctly is delegatecall. In practice, Safe makes heavy use of delegatecalls to enable common use cases like batching multiple actions and transactions, ie by delegatecalling their MultiSend library. That library does not present a security issue, but the need to allow delegatecalls does.
+
+If the signers were to execute a delegatecall to a contract with certain logic, they could directly update the Safe’s state, bypassing the typical Safe functions to do so. This is an issue because two of the critical types of Safe state changes we want to avoid — owners and modules — are mappings and therefore infeasible to explicitly check onchain. For example, a malicious delegatecalled contract could change the value for an address’s key in the owner mapping such that Safe’s logic would treat the address as a valid owner but not retrieve it as part of the owner linked list. The same issue exists for modules.
+
+### C) Updating the proxy to a malicious Safe singleton
+
+Also due to the need to allow arbitrary delegatecalls, the signers could update the Safe proxy to point to a different Safe singleton (aka “master copy” or “implementation”) contract that exposes sensitive functionality to an attacker.
+
+### D) Setting a malicious fallback handler
+
+Safe has a modular fallback concept that enables extensions to the Safe’s functionality via setting a fallback handler contract. The fallback handler deployed with new Safes by default, for example, is where ERC1271 compliance is implemented. In regular usage, the Safe itself (ie the signers or a module) can update the fallback handler to an arbitrary contract address.
+
+But if the signers (or a rogue module; see A) did this, they could gain the same unmediated access to Safe state as adding a module.
+
+### E) Sub-optimal gas overhead
+
+Some of the techniques we used to implement our desired logic and protections in v1 were not ideal, leading to what we now think is an extra 20k-40k gas cost for every transaction executed by signers.
+
+This is not that big of a deal on L2s, but on L1 it imposes meaningful costs on users and limits the potential market for what we’re building.
+
+### F) Valid threshold legibility
+
+HSG enables an organization to control the decision model for the delegated account. In other words, in addition to controlling who the signers are, HSG controls how many signers are needed to execute a transaction, ie the multisig threshold N. Because the Safe only validates N signatures regardless of how many are included in the signature bytes array of a multisig transaction, HSG needs to keep the Safe threshold up to date in accordance with the number of presently hat-wearing signers. But since hats can be revoked dynamically (see the Hats Protocol intro above), HSG is not always aware of such a change. This means that HSG needs to check whether each signer is still wearing the hat whenever it tries to update the Safe’s threshold, contributing to some of the gas overhead (see (E)) and otherwise increasing complexity.
+
+### G) Inability to remove HSG
+
+To give signers some form of credible expectations, we didn’t allow orgs to detach HSG from the Safe. This was a mistake, since it added significant friction when deciding whether to try HSG in the first place.
+
+### Implications for v2
+
+For (A), we need to find a solution to this problem. Part of our ethos is a deep love for open source composability, and the HSG v1 does not meet that bar.
+
+Despite the vulnerabilities outlined above in (B), (C), and (D), we still consider HSG v1 sufficiently secure. They imply an assumption that the signers are semi-trusted, which today is generally the case anyways for a typical multisig within an organization.
+
+But we’re not satisfied with this scenario. Part of our mission is to enable organizations to delegate authorities and responsibilities to operators across the full trust spectrum. To do so for multisig signing permissions, therefore, we want to close down these vulnerabilities as best we can without harming legitimate delegated operations.
+
+## Differences between v1 and v2
+
+Here’s what we’re changing in v2 to address the limitations described above.
+
+### 1) Simpler, more legible threshold logic
+
+HSG v2 will set the Safe's threshold to the lower of the following:
+
+- The number of current owners on the Safe (the number of "static signers")
+- The required number of valid signatures to execute a transaction
+
+Since (b) is a function of (a), this means that the threshold value set in Safe storage is independent of whether the Safe owners are wearing one of the signer Hats. As a result, unlike in v1, there will never be a discrepancy between what the Safe threshold is and what it should be.
+
+One tradeoff is that the threshold according to the Safe is not necessarily the same as the number of valid signatures that will be enforced by HSG. If, for example, one or more of the Safe owners has lost their Hat, its possible that the actual number of required valid signatures is lower than the threshold set in Safe storage.
+
+### 2) Proportional threshold option
+
+In addition to the absolute approach to calculating the number of required valid signatures that v1 used, v2 introduces an option to calculate the number of valid signatures proportionally.
+
+The absolute approach is a good fit for when the number of signers is expected to be constant over time.
+
+The proportional approach is a good fit for when the number of signers is expected to float up and down according to some external factor.
+
+Both approaches can be configured with a minimum value to ensure the desired level of safety during signer transitions.
+
+### 3) HSG as a Zodiac modifier
+
+This addresses limitation A. Any Safe modules and guards can now be used on a Safe in conjunction HSG. HSG still needs to be the sole module and guard enabled directly on the Safe, but the HSG owner can enable additional modules and guards on HSG. In other words, HSG now serves as a Zodiac modifier.
+
+For a guard enabled on HSG, HSG’s own guard functions will include a call to the guard.
+
+For a module enabled on HSG, HSG exposes its own execTransactionFromModule function that forwards the call to Safe’s execTransactionFromModule. Note that these calls are subject to the same safety checks as those initiated by the signers.
+
+### 4) Only allow delegatecalls to approved targets
+
+This addresses limitations B and C. In HSG v2, the owner can set a list of approved targets for delegatecalls. Delegatecalls to targets not on this list are rejected.
+
+HSG is deployed by default with Safe’s MultiSendCallOnly library for Safe versions v1.3.0 and v1.4.1. This enables batched transactions from Safe apps without any custom configuration.
+
+The owner is trusted to not approve malicious or unsafe contracts. For example, they should not approve the MultiSend library, since that would allow arbitrary delegatecalls.
+
+### 5) Prevent changes to the fallback handler
+
+HSG v2’s suite of safety checks prevents changes to the fallback handler.
+
+### 6) Leverage transient storage for gas savings
+
+In many of its safety checks, HSG stores a copy of Safe storage values to ensure they are not changed by a Safe transaction. In v2, this is done with transient storage for significant gas savings.
+
+Note that this means that HSG v2 cannot be deployed to chains that do have support for TSTORE or TLOAD (EIP 1153).
+
+### 7) Better reentrancy logic
+
+Reentrancy creates a specific type of vulnerability in HSG. To prevent Safe signers and modules from changing any Safe state, HSG temporarily stores a pre-execution copy of the values of key state (owners, threshold, tx operation, and fallback handler). If those values are manipulated by reentering the functions where they are set, the protections would break down.
+
+In v1, there was only one function that set these pre-execution values. In v2, there are two: i) checkTransaction, and ii) execTransactionFromModule and execTransactionFromModuleReturnData.
+
+In (ii), we can use a fairly standard reentrancy guard: revert if either execTransactionFromModule or execTransactionFromModuleReturnData have already been entered. Our design should allow multiple legitimate external calls to one or both functions, but disallow reentrance from within the same call that could override our copy of the Safe state.
+
+Since (i) is called as part of a multi-call flow originating from Safe.execTransaction, we need a slightly different approach. The goal is to ensure the checkTransaction is only called once per time that Safe.execTransaction is called. If checkTransaction is called more, our cached state could be overwritten. To do this, we use the fact that the Safe nonce increments only from within the execTransaction to calculate how many times Safe.execTransaction has been called and compare that to a count of how many times checkTransaction has been entered.
+
+Note that there is a relationship between (i) and (ii). We also need to ensure that (i) is not entered from within a call to (ii), and vice versa.
+
+### 8) Only 1 implementation: all HSGs support multiple signer hats
+
+To simplify the implementation, HSG v2 supports multiple signer hats. This means that all signers must register the hat with which they are claiming their signer permission.
+
+### 9) No max signers value
+
+v1 had a max signers value to ensure that there were no more than the desired number of signers. This was redundant, since the maximum could also be managed via the max supply of the signer hat(s). v2 removes this redundant requirement, simplifying some of the logic and making it easier to get started.
+
+### 10) Detaching and migrating
+
+This addresses limitation G. In v2, the owner can now detach HSG from the Safe, handing over full control over the Safe to its existing signers.
+
+The owner can also choose to migrate the Safe from one HSG version to another. This is useful for upgrading to a future v3 of HSG, or if the owner wants to start clean with a new instance for whatever reason. When migrating, the owner can include a list of signers to migrate.
+
+### 11) Locking
+
+In HSG v2, the owner can “lock” the HSG, disabling any further changes by the owner. This is useful for scenarios where removing all trust from the system or eliminating uncertainty is valuable.
+
+### 12) Claiming For
+
+In HSG v2, the owner can optionally allow signer permissions to be claimed on behalf of accounts wearing a signer hat. This option removes the often-desired requirement for signers to opt in to the responsibility being a signer on a multisig, but adds the ability to integrate signer permissions claiming with external actions.
+
+### 13) No dedicated factory
+
+In contrast to the dedicated factory used by v1, v2 is deployed via the Zodiac Module Factory. This makes it easier to deploy HSG to new chains and will also make it easier to deploy new versions or future flavors of HSG.
+
+To enable paired deployment of HSG with a Safe, the Safe can optionally be deployed and configured from within HSG’s setUp function.
diff --git a/audits/Hats_Audit_Report_Sherlock.pdf b/docs/audit-v1/Hats_Audit_Report_Sherlock.pdf
similarity index 100%
rename from audits/Hats_Audit_Report_Sherlock.pdf
rename to docs/audit-v1/Hats_Audit_Report_Sherlock.pdf
diff --git a/audits/TrustSecurity_HatsProtocol_v02.pdf b/docs/audit-v1/TrustSecurity_HatsProtocol_v02.pdf
similarity index 100%
rename from audits/TrustSecurity_HatsProtocol_v02.pdf
rename to docs/audit-v1/TrustSecurity_HatsProtocol_v02.pdf
diff --git a/docs/audit-v2/HatsSignerGate v2 Audit Report.pdf b/docs/audit-v2/HatsSignerGate v2 Audit Report.pdf
new file mode 100644
index 0000000..08c1c98
Binary files /dev/null and b/docs/audit-v2/HatsSignerGate v2 Audit Report.pdf differ
diff --git a/example.env b/example.env
index 18b9306..e104474 100644
--- a/example.env
+++ b/example.env
@@ -1,11 +1,4 @@
-GOERLI_RPC=
-RINKEBY_RPC=
GC_RPC=
-POLYGON_RPC=
-ETHEREUM_RPC=
-OPTIMISM_RPC=
-SEPOLIA_RPC=
+INFURA_KEY=
export PRIVATE_KEY=
ETHERSCAN_KEY=
-GNOSISSCAN_KEY=
-POLYGONSCAN_KEY=
diff --git a/foundry.toml b/foundry.toml
index 45b44b6..ac90640 100644
--- a/foundry.toml
+++ b/foundry.toml
@@ -7,7 +7,8 @@ optimizer_runs = 1_000_000
bytecode_hash = "none"
gas_reports = ["*"]
auto_detect_solc = false
-solc = "0.8.17"
+solc = "0.8.28"
+evm_version = "cancun"
fs_permissions = [{ access = "read", path = "./"}]
remappings = [
"solmate/=lib/solmate/src/",
@@ -15,11 +16,13 @@ remappings = [
"ERC1155/=lib/ERC1155/",
"@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
"@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts",
- "@gnosis.pm/safe-contracts/contracts/=lib/safe-contracts/contracts/",
+ "safe-smart-account/safe-contracts/contracts/=lib/safe-contracts/contracts/",
+ "@gnosis.pm/safe-contracts/contracts=lib/safe-smart-account/contracts/",
"@gnosis.pm/zodiac/=lib/zodiac/contracts/",
"solbase/=lib/solbase/src/",
"zodiac/=lib/zodiac/contracts/",
- "hats-protocol/=lib/hats-protocol/src/"
+ "hats-protocol/=lib/hats-protocol/src/",
+ "hats-auth/=lib/hats-auth/src/"
]
[fmt]
@@ -27,27 +30,30 @@ bracket_spacing = true
int_types = "long"
line_length = 120
multiline_func_header = "attributes_first"
-number_underscore = "preserve"
+number_underscore = "thousands"
quote_style = "double"
-tab_width = 4
-wrap_comments = false
+tab_width = 2
+wrap_comments = true
[rpc_endpoints]
-arbitrum = "${ARBITRUM_RPC}"
-ethereum = "${ETHEREUM_RPC}"
-optimism = "${OPTIMISM_RPC}"
-goerli = "${GOERLI_RPC}"
+arbitrum = "https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}"
+base = "https://base-mainnet.infura.io/v3/${INFURA_KEY}"
+celo = "https://celo-mainnet.infura.io/v3/${INFURA_KEY}"
gnosis = "${GC_RPC}"
-polygon = "${POLYGON_RPC}"
-sepolia = "${SEPOLIA_RPC}"
+local = "http://localhost:8545"
+mainnet = "https://mainnet.infura.io/v3/${INFURA_KEY}"
+optimism = "https://optimism-mainnet.infura.io/v3/${INFURA_KEY}"
+polygon = "https://polygon-mainnet.infura.io/v3/${INFURA_KEY}"
+sepolia = "https://sepolia.infura.io/v3/${INFURA_KEY}"
[etherscan]
-arbitrum = {key = "${ARBISCAN_KEY}", url = "https://api.arbiscan.io/api"}
-goerli = {key = "${ETHERSCAN_KEY}", url = "https://api-goerli.etherscan.io/api"}
-ethereum = {key = "${ETHERSCAN_KEY}", url = "https://api.etherscan.io/api"}
-optimism = {key = "${OPTIMISM_ETHERSCAN_KEY}", url = "https://api-optimistic.etherscan.io/api"}
-polygon = {key = "${POLYGONSCAN_KEY}", url = "https://api.polygonscan.com/api"}
-gnosis = {key = "${GNOSISSCAN_KEY}", url = "https://api.gnosisscan.io/api"}
-sepolia = {key = "${ETHERSCAN_KEY}", url = "https://api-sepolia.etherscan.io/api"}
+arbitrum = {key = "${ETHERSCAN_KEY}", url = "https://api.etherscan.io/v2/api?chainid=42161"}
+base = {key = "${ETHERSCAN_KEY}", url = "https://api.etherscan.io/v2/api?chainid=8453"}
+celo = {key = "${ETHERSCAN_KEY}", url = "https://api.etherscan.io/v2/api?chainid=42220"}
+gnosis = {key = "${ETHERSCAN_KEY}", url = "https://api.etherscan.io/v2/api?chainid=100"}
+mainnet = {key = "${ETHERSCAN_KEY}", url = "https://api.etherscan.io/v2/api?chainid=1"}
+optimism = {key = "${ETHERSCAN_KEY}", url = "https://api.etherscan.io/v2/api?chainid=10"}
+polygon = {key = "${ETHERSCAN_KEY}", url = "https://api.etherscan.io/v2/api?chainid=137"}
+sepolia = {key = "${ETHERSCAN_KEY}", url = "https://api.etherscan.io/v2/api?chainid=11155111"}
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
diff --git a/gasreport.ansi b/gasreport.ansi
new file mode 100644
index 0000000..188a639
--- /dev/null
+++ b/gasreport.ansi
@@ -0,0 +1,385 @@
+No files changed, compilation skipped
+
+Ran 2 tests for test/HatsSignerGate.internals.t.sol:AuthInternals
+[PASS] test_happy_checkOwner() (gas: 34286)
+[PASS] test_happy_checkUnlocked() (gas: 10262)
+Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.77s (406.21µs CPU time)
+
+Ran 1 test for test/HatsSignerGate.t.sol:DetachingHSG
+[PASS] test_happy_detachHSG() (gas: 218630)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.89s (136.84ms CPU time)
+
+Ran 2 tests for test/HatsSignerGate.moduleTxs.sol:ExecutingFromModuleViaHSG
+[PASS] test_happy_executionFailure() (gas: 66230)
+[PASS] test_happy_executionSuccess() (gas: 103519)
+Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.97s (80.78ms CPU time)
+
+Ran 1 test for test/HatsSignerGate.signerTxs.sol:HSGGuarding
+[PASS] test_executed() (gas: 433155)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.56s (54.93ms CPU time)
+
+Ran 7 tests for test/HatsSignerGate.signerTxs.sol:ConstrainingSigners
+[PASS] testCannotDecreaseThreshold() (gas: 1092698)
+[PASS] testCannotDisableGuard() (gas: 858428)
+[PASS] testCannotDisableModule() (gas: 877441)
+[PASS] testCannotIncreaseThreshold() (gas: 1092686)
+[PASS] testSignersCannotAddOwners() (gas: 1126662)
+[PASS] testSignersCannotRemoveOwners() (gas: 1109093)
+[PASS] testSignersCannotSwapOwners() (gas: 1131591)
+Suite result: ok. 7 passed; 0 failed; 0 skipped; finished in 4.31s (2.56s CPU time)
+
+Ran 7 tests for test/HatsSignerGate.signerTxs.sol:ExecutingTransactions
+[PASS] testExecByLessThanMinThresholdReverts() (gas: 877579)
+[PASS] testExecTxByHatWearers() (gas: 1203260)
+[PASS] testExecTxByNonHatWearersReverts() (gas: 1230247)
+[PASS] testExecTxByTooFewOwnersReverts() (gas: 458838)
+[PASS] test_Multi_ExecTxByHatWearers() (gas: 1231547)
+[PASS] test_Multi_ExecTxByNonHatWearersReverts() (gas: 1260795)
+[PASS] test_happy_delegateCall() (gas: 1272654)
+Suite result: ok. 7 passed; 0 failed; 0 skipped; finished in 4.45s (2.70s CPU time)
+
+Ran 8 tests for test/HatsSignerGate.attacks.t.sol:AttacksScenarios
+[PASS] testAttackerCannotExploitSigHandlingDifferences() (gas: 1576164)
+[PASS] testCanClaimToReplaceInvalidSignerAtMaxSigner() (gas: 1460066)
+[PASS] testRemoveSignerCorrectlyUpdates() (gas: 1427846)
+[PASS] testSetTargetThresholdCannotSetBelowMinThreshold() (gas: 68793)
+[PASS] testSetTargetThresholdUpdatesThresholdCorrectly() (gas: 1393389)
+[PASS] testSignersCannotAddNewModules() (gas: 896764)
+[PASS] testSignersCannotReenterCheckTransactionToAddOwners() (gas: 1250647)
+[PASS] testTargetSigAttackFails() (gas: 1871128)
+Suite result: ok. 8 passed; 0 failed; 0 skipped; finished in 6.41s (4.65s CPU time)
+
+Ran 1 test for test/HatsSignerGate.t.sol:CheckAfterExecution
+[PASS] test_happy_checkAfterExecution(bytes32,bool) (runs: 256, μ: 59484, ~: 59606)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 16.22s (14.45s CPU time)
+
+Ran 2 tests for test/HatsSignerGate.t.sol:CheckTransaction
+[PASS] test_delegatecallTargetEnabled() (gas: 337618)
+[PASS] test_happy_checkTransaction_callToNonSafe(uint256) (runs: 256, μ: 190169, ~: 190170)
+Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 16.71s (14.70s CPU time)
+
+Ran 3 tests for test/HatsSignerGate.internals.t.sol:TransactionValidationInternals
+[FAIL: CannotChangeThreshold()] test_checkSafeState() (gas: 112477)
+[PASS] test_fuzz_checkModuleTransaction_callToNonSafeTarget(uint8) (runs: 256, μ: 40102, ~: 40103)
+[FAIL: the existing owners hash should be unchanged: 0x0000000000000000000000000000000000000000000000000000000000000000 != 0xdec22d665cd8a5d6a8e2fb5e36109e7c69a9fde85d7436b5081a9c4cc04c0d29; counterexample: calldata=0x9d37a228000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000079e8 args=[8, 1, 5, 3, 31208 [3.12e4]]] test_fuzz_checkModuleTransaction_delegatecallToApprovedTarget(uint8,uint8,uint8,uint8,uint16) (runs: 0, μ: 0, ~: 0)
+Suite result: FAILED. 1 passed; 2 failed; 0 skipped; finished in 16.41s (14.91s CPU time)
+
+Ran 1 test for test/HatsSignerGate.t.sol:AddingSignerHats
+[PASS] test_fuzz_happy_addSignerHats(uint8) (runs: 256, μ: 616786, ~: 480218)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 30.96s (29.49s CPU time)
+
+Ran 1 test for test/HatsSignerGate.t.sol:DisablingDelegatecallTarget
+[PASS] test_fuzz_happy_disableDelegatecallTarget(uint256) (runs: 256, μ: 264260, ~: 264348)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 45.24s (43.95s CPU time)
+
+Ran 8 tests for test/HatsSignerGate.internals.t.sol:OwnerSettingsInternals
+[PASS] test_addSignerHats_duplicateHats() (gas: 62732)
+[PASS] test_addSignerHats_emptyArray() (gas: 35944)
+[PASS] test_fuzz_addSignerHats(uint8) (runs: 256, μ: 453318, ~: 330467)
+[PASS] test_fuzz_setClaimableFor(bool) (runs: 256, μ: 40112, ~: 41497)
+[PASS] test_fuzz_setDelegatecallTarget(uint256,bool) (runs: 256, μ: 90055, ~: 93244)
+[PASS] test_fuzz_setOwnerHat(uint256) (runs: 257, μ: 41211, ~: 41328)
+[PASS] test_fuzz_setThresholdConfig_valid(uint8,uint120,uint120) (runs: 256, μ: 47369, ~: 47380)
+[PASS] test_lock() (gas: 39277)
+Suite result: ok. 8 passed; 0 failed; 0 skipped; finished in 60.59s (88.57s CPU time)
+
+Ran 2 tests for test/HatsSignerGate.moduleTxs.sol:ExecutingFromModuleReturnDataViaHSG
+[PASS] test_happy_executionFailure() (gas: 67690)
+[PASS] test_happy_executionSuccess() (gas: 104979)
+Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.66s (119.46ms CPU time)
+
+Ran 2 tests for test/HatsSignerGate.internals.t.sol:RegisterSignerInternals
+[PASS] test_fuzz_happy_registerSigner_allowRegistration(uint256,uint8) (runs: 256, μ: 129332, ~: 129317)
+[PASS] test_fuzz_happy_registerSigner_disallowRegistration(uint256,uint8,uint256) (runs: 256, μ: 204740, ~: 204753)
+Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 74.63s (72.86s CPU time)
+
+Ran 2 tests for test/HatsSignerGate.t.sol:DisablingModule
+[PASS] test_happy_disableModule(uint256) (runs: 257, μ: 274605, ~: 274731)
+[PASS] test_happy_disableModule_twoModules(uint256) (runs: 257, μ: 352851, ~: 352979)
+Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 104.29s (102.72s CPU time)
+
+Ran 1 test for test/HatsSignerGate.t.sol:EnablingDelegatecallTarget
+[PASS] test_fuzz_happy_enableDelegatecallTarget(uint256) (runs: 257, μ: 216595, ~: 216682)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 30.73s (29.27s CPU time)
+
+Ran 3 tests for test/HatsSignerGate.t.sol:ClaimingSignerFor
+[PASS] test_alreadyOwner_notRegistered(uint256) (runs: 256, μ: 339864, ~: 339866)
+[PASS] test_alreadyRegistered_notWearingRegisteredHat(uint256) (runs: 257, μ: 516046, ~: 516050)
+[PASS] test_happy_claimSignerFor(uint256) (runs: 257, μ: 313371, ~: 313373)
+Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 192.40s (190.99s CPU time)
+
+Ran 1 test for test/HatsSignerGate.t.sol:EnablingModule
+[PASS] test_happy_enableModule(uint256) (runs: 257, μ: 221590, ~: 221661)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 30.89s (29.40s CPU time)
+
+Ran 8 tests for test/SafeManagerLib.t.sol:SafeManagerLib_EncodingActions
+[PASS] test_fuzz_encodeAddOwnerWithThresholdAction(address,uint256) (runs: 257, μ: 4043, ~: 4043)
+[PASS] test_fuzz_encodeChangeThresholdAction(uint256) (runs: 257, μ: 3874, ~: 3874)
+[PASS] test_fuzz_encodeDisableModuleAction(address,address) (runs: 257, μ: 4161, ~: 4161)
+[PASS] test_fuzz_encodeEnableModuleAction(address) (runs: 257, μ: 3958, ~: 3958)
+[PASS] test_fuzz_encodeRemoveHSGAsGuardAction() (gas: 3810)
+[PASS] test_fuzz_encodeRemoveOwnerAction(address,address,uint256) (runs: 257, μ: 4245, ~: 4245)
+[PASS] test_fuzz_encodeSetGuardAction(address) (runs: 257, μ: 4002, ~: 4002)
+[PASS] test_fuzz_encodeSwapOwnerAction(address,address,address) (runs: 257, μ: 4245, ~: 4245)
+Suite result: ok. 8 passed; 0 failed; 0 skipped; finished in 20.45ms (20.37ms CPU time)
+
+Ran 1 test for test/HatsSignerGate.t.sol:HSGGuarding
+[PASS] test_executed() (gas: 433155)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.86s (57.34ms CPU time)
+
+Ran 3 tests for test/HatsSignerGate.t.sol:ImplementationDeployment
+[PASS] test_constructorArgs() (gas: 21832)
+[PASS] test_ownerHat() (gas: 10491)
+[PASS] test_version() (gas: 9360)
+Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 1.22s (169.08µs CPU time)
+
+Ran 1 test for test/HatsSignerGate.t.sol:RemovingSigner
+[PASS] test_happy_removeSigner(uint256) (runs: 257, μ: 374906, ~: 374908)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 60.49s (59.04s CPU time)
+
+Ran 2 tests for test/HatsSignerGate.internals.t.sol:RemovingSignerInternals
+[PASS] test_fuzz_removeSigner(uint8) (runs: 257, μ: 1832737, ~: 1302599)
+[PASS] test_fuzz_removeSigner_lastSigner(uint8) (runs: 257, μ: 181697, ~: 181698)
+Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 195.77s (223.84s CPU time)
+
+Ran 4 tests for test/HatsSignerGate.internals.t.sol:AddingSignerInternals
+[PASS] test_fuzz_addSigner_alreadySigner(uint8) (runs: 256, μ: 141828, ~: 141830)
+[PASS] test_fuzz_addSigner_firstSigner(uint8) (runs: 256, μ: 99003, ~: 99004)
+[PASS] test_fuzz_addSigner_happy(uint8,uint8) (runs: 256, μ: 1408363, ~: 808540)
+[PASS] test_fuzz_addSigner_secondSigner_notSigner(uint8,uint8) (runs: 256, μ: 203119, ~: 203121)
+Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 241.29s (228.01s CPU time)
+
+Ran 1 test for test/SafeManagerLib.t.sol:SafeManagerLib_DeployingSafeAndAttachingHSG
+[PASS] test_deploySafeAndAttachHSG() (gas: 6646719)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.34s (119.20ms CPU time)
+
+Ran 1 test for test/HatsSignerGate.t.sol:Locking
+[PASS] test_happy_lock() (gas: 193172)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.59s (114.55ms CPU time)
+
+Ran 6 tests for test/SafeManagerLib.t.sol:SafeManagerLib_Views
+[PASS] test_canAttachHSG_false() (gas: 19652)
+[PASS] test_canAttachHSG_true() (gas: 275125)
+[PASS] test_findPrevOwner() (gas: 5505190)
+[PASS] test_getModulesWith1() (gas: 23225)
+[PASS] test_getSafeFallbackHandler() (gas: 1983153)
+[PASS] test_getSafeGuard() (gas: 18976)
+Suite result: ok. 6 passed; 0 failed; 0 skipped; finished in 6.32s (4.78s CPU time)
+
+Ran 1 test for test/HatsSignerGate.t.sol:SettingClaimableFor
+[PASS] test_fuzz_happy_setClaimableFor(bool) (runs: 257, μ: 192806, ~: 194185)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 30.92s (29.45s CPU time)
+
+Ran 1 test for test/HatsSignerGate.t.sol:SettingGuard
+[PASS] test_happy_setGuard(uint256) (runs: 257, μ: 269446, ~: 269446)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 45.35s (43.89s CPU time)
+
+Ran 5 tests for test/HatsSignerGate.t.sol:ClaimingSigner
+[PASS] test_fuzz_claimSigner_alreadyRegistered_differentHats(uint256) (runs: 256, μ: 412549, ~: 412551)
+[PASS] test_fuzz_claimSigner_alreadyRegistered_sameHat(uint256) (runs: 257, μ: 316165, ~: 316166)
+[PASS] test_fuzz_claimSigner_notRegistered_onSafe(uint256) (runs: 257, μ: 280189, ~: 280191)
+[PASS] test_fuzz_happy_claimSigner(uint256) (runs: 201, μ: 253702, ~: 253703)
+[PASS] test_fuzz_multipleSigners_multipleHats(uint256,uint256) (runs: 257, μ: 1344961, ~: 1322429)
+Suite result: ok. 5 passed; 0 failed; 0 skipped; finished in 301.57s (330.81s CPU time)
+
+Ran 1 test for test/HatsSignerGate.t.sol:SettingOwnerHat
+[PASS] test_fuzz_happy_setOwnerHat(uint256) (runs: 257, μ: 193841, ~: 193956)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 30.47s (28.98s CPU time)
+
+Ran 1 test for test/HatsSignerGate.t.sol:SettingThresholdConfig
+[PASS] test_fuzz_happy_setThresholdConfig(uint8,uint8,uint16) (runs: 257, μ: 235554, ~: 235551)
+Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 27.25s (25.79s CPU time)
+
+Ran 2 tests for test/HatsSignerGate.t.sol:InstanceDeployment
+[PASS] test_initialParams_existingSafe(bool,bool) (runs: 257, μ: 3249682, ~: 3249191)
+[PASS] test_initialParams_newSafe(bool,bool) (runs: 257, μ: 3326320, ~: 3331405)
+Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 101.87s (131.89s CPU time)
+
+Ran 8 tests for test/HatsSignerGate.internals.t.sol:ViewInternals
+[PASS] test_fuzz_countValidSigners(uint8) (runs: 257, μ: 1659436, ~: 1265024)
+[PASS] test_fuzz_getNewThreshold(uint8,uint8,uint16,uint16) (runs: 257, μ: 52657, ~: 52703)
+[PASS] test_fuzz_getNewThreshold_exceedsOwnerCount(uint8,uint8,uint16) (runs: 257, μ: 51144, ~: 51139)
+[PASS] test_fuzz_getRequiredValidSignatures_absolute(uint8,uint16,uint16) (runs: 257, μ: 49366, ~: 49313)
+[PASS] test_fuzz_getRequiredValidSignatures_absolute_ownerCountIsMin(uint8,uint16) (runs: 257, μ: 49262, ~: 49131)
+[PASS] test_fuzz_getRequiredValidSignatures_absolute_targetOwnerCount(uint8,uint16) (runs: 257, μ: 49328, ~: 49197)
+[PASS] test_fuzz_getRequiredValidSignatures_ownerCountLtMin(uint8,uint8,uint16) (runs: 257, μ: 51126, ~: 51121)
+[PASS] test_fuzz_getRequiredValidSignatures_proportional(uint8,uint16,uint16) (runs: 257, μ: 51158, ~: 50999)
+Suite result: ok. 8 passed; 0 failed; 0 skipped; finished in 316.08s (417.47s CPU time)
+
+Ran 3 tests for test/HatsSignerGate.t.sol:Views
+[PASS] test_false_canAttachToSafe(uint256) (runs: 257, μ: 813627, ~: 813628)
+[PASS] test_fuzz_canAttachToSafe() (gas: 751870)
+[PASS] test_fuzz_validSignerCount(uint256) (runs: 257, μ: 2528957, ~: 2676120)
+Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 217.03s (215.50s CPU time)
+
+Ran 2 tests for test/HatsSignerGate.t.sol:MigratingToNewHSG
+[PASS] test_happy_noSignersToMigrate() (gas: 280867)
+[PASS] test_happy_signersToMigrate(uint256) (runs: 257, μ: 3313913, ~: 3217606)
+Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 185.09s (183.39s CPU time)
+
+Ran 3 tests for test/HatsSignerGate.t.sol:ClaimingSignersFor
+[PASS] test_alreadyOwnerNotRegistered_happy(uint256) (runs: 257, μ: 2081673, ~: 1980770)
+[PASS] test_startingEmpty_happy(uint256) (runs: 257, μ: 1752542, ~: 1670221)
+[PASS] test_startingWith1Signer_happy(uint256) (runs: 257, μ: 1826677, ~: 1746526)
+Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 327.07s (521.47s CPU time)
+
+Ran 6 tests for test/SafeManagerLib.t.sol:SafeManagerLib_ExecutingActions
+[PASS] test_execAttachNewHSG() (gas: 108422)
+[PASS] test_execDisableHSGAsOnlyModule() (gas: 58646)
+[PASS] test_execRemoveHSGAsGuard() (gas: 51239)
+[PASS] test_fuzz_execChangeThreshold(uint256) (runs: 257, μ: 1860772, ~: 2116033)
+[PASS] test_fuzz_execDisableHSGAsModule(uint256) (runs: 257, μ: 1493220, ~: 1412971)
+[PASS] test_fuzz_fail_execChangeThreshold_tooHigh(uint256) (runs: 257, μ: 1583090, ~: 1582366)
+Suite result: ok. 6 passed; 0 failed; 0 skipped; finished in 238.53s (528.72s CPU time)
+
+Ran 4 tests for test/HatsSignerGate.internals.t.sol:CountingValidSignaturesInternals
+[PASS] test_fuzz_countValidSignatures_approvedHash(uint256) (runs: 256, μ: 1090712, ~: 1067058)
+[PASS] test_fuzz_countValidSignatures_contractSignature(uint256) (runs: 257, μ: 1091265, ~: 1066879)
+[PASS] test_fuzz_countValidSignatures_default(bytes32,uint256) (runs: 256, μ: 1289598, ~: 1394821)
+[PASS] test_fuzz_countValidSignatures_ethSign(bytes32,uint256) (runs: 257, μ: 1294101, ~: 1400360)
+Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 452.18s (899.04s CPU time)
+| script/HatsSignerGate.s.sol:DeployImplementation contract | | | | | |
+|-----------------------------------------------------------|-----------------|---------|---------|---------|---------|
+| Deployment Cost | Deployment Size | | | | |
+| 6495455 | 29898 | | | | |
+| Function Name | min | avg | median | max | # calls |
+| hats | 400 | 400 | 400 | 400 | 111 |
+| prepare | 26507 | 26507 | 26507 | 26507 | 111 |
+| run | 5175332 | 5175332 | 5175332 | 5175332 | 111 |
+| safeFallbackLibrary | 346 | 346 | 346 | 346 | 111 |
+| safeMultisendLibrary | 345 | 345 | 345 | 345 | 111 |
+| safeProxyFactory | 347 | 347 | 347 | 347 | 111 |
+| safeSingleton | 368 | 368 | 368 | 368 | 111 |
+| zodiacModuleFactory | 346 | 346 | 346 | 346 | 111 |
+
+
+| script/HatsSignerGate.s.sol:DeployInstance contract | | | | | |
+|-----------------------------------------------------|-----------------|--------|--------|---------|---------|
+| Deployment Cost | Deployment Size | | | | |
+| 1617977 | 7222 | | | | |
+| Function Name | min | avg | median | max | # calls |
+| prepare1 | 345088 | 456479 | 473195 | 473435 | 577 |
+| prepare2 | 46041 | 48819 | 48829 | 48829 | 577 |
+| run | 598310 | 856149 | 893615 | 1000778 | 577 |
+
+
+| src/HatsSignerGate.sol:HatsSignerGate contract | | | | | |
+|------------------------------------------------|-----------------|--------|--------|---------|---------|
+| Deployment Cost | Deployment Size | | | | |
+| 0 | 0 | | | | |
+| Function Name | min | avg | median | max | # calls |
+| HATS | 315 | 315 | 315 | 315 | 513 |
+| addSignerHats | 48193 | 393814 | 274944 | 1159290 | 256 |
+| canAttachToSafe | 2890 | 3570 | 3573 | 3573 | 257 |
+| checkAfterExecution | 716 | 8819 | 8367 | 18164 | 15 |
+| checkTransaction | 3944 | 66392 | 71633 | 80561 | 22 |
+| claimSigner | 37812 | 112873 | 115277 | 156982 | 8714 |
+| claimSignerFor | 52485 | 86401 | 89436 | 114248 | 1024 |
+| claimSignersFor | 24653 | 545538 | 511105 | 1438687 | 1024 |
+| claimableFor | 421 | 1421 | 1421 | 2421 | 1536 |
+| detachHSG | 66410 | 66410 | 66410 | 66410 | 1 |
+| disableDelegatecallTarget | 30199 | 30199 | 30199 | 30199 | 256 |
+| disableModule | 35507 | 35507 | 35507 | 35507 | 512 |
+| enableDelegatecallTarget | 47279 | 47279 | 47279 | 47279 | 512 |
+| enableModule | 52438 | 52438 | 52438 | 52438 | 1028 |
+| enabledDelegatecallTargets | 590 | 590 | 590 | 590 | 2048 |
+| execTransactionFromModule | 25501 | 41232 | 41232 | 56963 | 2 |
+| execTransactionFromModuleReturnData | 26496 | 42227 | 42227 | 57958 | 2 |
+| getGuard | 395 | 395 | 395 | 395 | 1026 |
+| getModulesPaginated | 2888 | 2888 | 2888 | 2888 | 512 |
+| getSafeDeployParamAddresses | 343 | 343 | 343 | 343 | 1 |
+| implementation | 380 | 380 | 380 | 380 | 512 |
+| isModuleEnabled | 660 | 660 | 660 | 660 | 768 |
+| isValidSigner | 4179 | 4201 | 4179 | 4718 | 6044 |
+| isValidSignerHat | 502 | 502 | 502 | 502 | 9278 |
+| lock | 27419 | 27419 | 27419 | 27419 | 1 |
+| locked | 398 | 398 | 398 | 398 | 513 |
+| migrateToNewHSG | 112816 | 523179 | 519136 | 899182 | 257 |
+| ownerHat | 384 | 386 | 384 | 2384 | 769 |
+| registeredSignerHats | 565 | 565 | 565 | 565 | 3446 |
+| removeSigner | 80200 | 85316 | 85336 | 85336 | 257 |
+| safe | 425 | 425 | 425 | 425 | 831 |
+| setClaimableFor | 25052 | 27681 | 27852 | 27852 | 2048 |
+| setGuard | 29902 | 40124 | 50268 | 50268 | 514 |
+| setOwnerHat | 27644 | 27644 | 27644 | 27644 | 256 |
+| setThresholdConfig | 24312 | 61662 | 61656 | 74895 | 261 |
+| supportsInterface | 441 | 441 | 441 | 441 | 576 |
+| thresholdConfig | 899 | 906 | 899 | 2899 | 516 |
+| validSignerCount | 7064 | 40894 | 38497 | 97404 | 1540 |
+| version | 495 | 495 | 495 | 495 | 1 |
+
+
+| test/harnesses/HatsSignerGateHarness.sol:HatsSignerGateHarness contract | | | | | |
+|-------------------------------------------------------------------------|-----------------|--------|--------|---------|---------|
+| Deployment Cost | Deployment Size | | | | |
+| 6245470 | 29331 | | | | |
+| Function Name | min | avg | median | max | # calls |
+| checkAfterExecution | 740 | 740 | 740 | 740 | 2 |
+| checkTransaction | 63980 | 63980 | 63980 | 63980 | 2 |
+| claimSigner | 107315 | 110867 | 110867 | 114419 | 4 |
+| claimableFor | 422 | 422 | 422 | 422 | 256 |
+| deploySafeAndAttachHSG | 318547 | 318547 | 318547 | 318547 | 1 |
+| enableDelegatecallTarget | 27422 | 27422 | 27422 | 27422 | 1 |
+| enabledDelegatecallTargets | 588 | 588 | 588 | 588 | 1 |
+| entrancyCounter | 428 | 428 | 428 | 428 | 257 |
+| execAttachNewHSG | 64387 | 64387 | 64387 | 64387 | 1 |
+| execChangeThreshold | 19785 | 22765 | 21431 | 25877 | 512 |
+| execDisableHSGAsModule | 28112 | 28112 | 28112 | 28112 | 256 |
+| execDisableHSGAsOnlyModule | 28061 | 28061 | 28061 | 28061 | 1 |
+| execRemoveHSGAsGuard | 23749 | 23749 | 23749 | 23749 | 1 |
+| existingFallbackHandler | 446 | 446 | 446 | 446 | 257 |
+| existingOwnersHash | 385 | 385 | 385 | 385 | 257 |
+| existingThreshold | 429 | 429 | 429 | 429 | 257 |
+| exposed_addSigner | 12933 | 80195 | 75711 | 143490 | 7456 |
+| exposed_addSignerHats | 2281 | 143235 | 47759 | 1139253 | 770 |
+| exposed_checkAfterExecution | 2729 | 2729 | 2729 | 2729 | 256 |
+| exposed_checkModuleTransaction | 659 | 760 | 659 | 26788 | 257 |
+| exposed_checkOwner | 21426 | 21426 | 21426 | 21426 | 1 |
+| exposed_checkSafeState | 6013 | 6013 | 6013 | 6013 | 1 |
+| exposed_checkTransaction | 104030 | 104291 | 104030 | 171204 | 257 |
+| exposed_checkUnlocked | 2396 | 2396 | 2396 | 2396 | 1 |
+| exposed_countValidSignatures | 4954 | 62346 | 55820 | 157787 | 1024 |
+| exposed_countValidSigners | 4924 | 68908 | 47670 | 208334 | 256 |
+| exposed_existingOwnersHash | 380 | 380 | 380 | 380 | 1 |
+| exposed_getNewThreshold | 1015 | 1200 | 1051 | 3015 | 6896 |
+| exposed_getRequiredValidSignatures | 985 | 1073 | 1012 | 1220 | 1536 |
+| exposed_lock | 6046 | 6046 | 6046 | 6046 | 1 |
+| exposed_registerSigner | 12652 | 49556 | 52019 | 52019 | 8136 |
+| exposed_removeSigner | 53455 | 72810 | 63382 | 132838 | 512 |
+| exposed_setClaimableFor | 3768 | 5189 | 6568 | 6568 | 256 |
+| exposed_setDelegatecallTarget | 4190 | 14814 | 24090 | 24090 | 513 |
+| exposed_setOwnerHat | 6442 | 6442 | 6442 | 6442 | 256 |
+| exposed_setThresholdConfig | 5200 | 7986 | 8000 | 8000 | 2049 |
+| initialNonce | 384 | 384 | 384 | 384 | 257 |
+| isValidSignerHat | 547 | 868 | 547 | 2547 | 4773 |
+| locked | 421 | 421 | 421 | 421 | 1 |
+| operation | 502 | 502 | 502 | 502 | 257 |
+| ownerHat | 386 | 386 | 386 | 386 | 256 |
+| reentrancyGuard | 407 | 407 | 407 | 407 | 257 |
+| registeredSignerHats | 588 | 588 | 588 | 588 | 512 |
+| safe | 425 | 425 | 425 | 425 | 42 |
+| setExistingFallbackHandler | 672 | 672 | 672 | 672 | 1 |
+| setExistingOwnersHash | 443 | 443 | 443 | 443 | 1 |
+| setExistingThreshold | 422 | 422 | 422 | 422 | 1 |
+| supportsInterface | 464 | 464 | 464 | 464 | 43 |
+| thresholdConfig | 966 | 966 | 966 | 966 | 256 |
+
+
+| test/mocks/TestGuard.sol:TestGuard contract | | | | | |
+|---------------------------------------------|-----------------|-----|--------|-----|---------|
+| Deployment Cost | Deployment Size | | | | |
+| 499117 | 2727 | | | | |
+| Function Name | min | avg | median | max | # calls |
+| supportsInterface | 350 | 350 | 350 | 350 | 771 |
+
+
+
+
+Ran 40 test suites in 452.71s (3426.38s CPU time): 117 tests passed, 2 failed, 0 skipped (119 total tests)
+
+Failing tests:
+Encountered 2 failing tests in test/HatsSignerGate.internals.t.sol:TransactionValidationInternals
+[FAIL: CannotChangeThreshold()] test_checkSafeState() (gas: 112477)
+[FAIL: the existing owners hash should be unchanged: 0x0000000000000000000000000000000000000000000000000000000000000000 != 0xdec22d665cd8a5d6a8e2fb5e36109e7c69a9fde85d7436b5081a9c4cc04c0d29; counterexample: calldata=0x9d37a228000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000079e8 args=[8, 1, 5, 3, 31208 [3.12e4]]] test_fuzz_checkModuleTransaction_delegatecallToApprovedTarget(uint8,uint8,uint8,uint8,uint16) (runs: 0, μ: 0, ~: 0)
+
+Encountered a total of 2 failing tests, 117 tests succeeded
diff --git a/lcov.info b/lcov.info
new file mode 100644
index 0000000..08e07c4
--- /dev/null
+++ b/lcov.info
@@ -0,0 +1,1386 @@
+TN:
+SF:script/HatsSignerGate.s.sol
+FN:13,BaseScript.getChainKey
+FNDA:844,BaseScript.getChainKey
+DA:14,844
+FN:35,DeployImplementation.setDeployParams
+FNDA:0,DeployImplementation.setDeployParams
+DA:36,200
+DA:37,200
+DA:38,200
+DA:39,200
+DA:41,200
+DA:44,200
+FN:48,DeployImplementation.prepare
+FNDA:200,DeployImplementation.prepare
+DA:49,200
+FN:52,DeployImplementation.run
+FNDA:200,DeployImplementation.run
+DA:53,200
+DA:54,200
+DA:55,200
+DA:56,200
+DA:58,200
+DA:61,200
+DA:63,0
+BRDA:63,0,0,-
+DA:64,0
+DA:65,0
+DA:67,0
+DA:68,0
+BRDA:68,1,0,-
+BRDA:68,1,1,-
+DA:69,0
+DA:71,0
+DA:74,0
+DA:75,0
+DA:76,0
+DA:77,0
+DA:80,200
+FN:102,DeployInstance.prepare1
+FNDA:644,DeployInstance.prepare1
+DA:113,644
+DA:114,644
+DA:115,644
+DA:116,644
+DA:117,644
+DA:118,644
+DA:119,644
+DA:120,644
+DA:121,644
+FN:124,DeployInstance.prepare2
+FNDA:644,DeployInstance.prepare2
+DA:125,644
+DA:126,644
+FN:129,DeployInstance.setModuleFactory
+FNDA:0,DeployInstance.setModuleFactory
+DA:130,644
+DA:131,644
+DA:132,644
+DA:133,644
+DA:135,644
+DA:138,644
+FN:141,DeployInstance.setupParams
+FNDA:0,DeployInstance.setupParams
+DA:142,644
+DA:153,0
+FN:156,DeployInstance.run
+FNDA:644,DeployInstance.run
+DA:157,644
+DA:159,644
+DA:160,644
+DA:161,644
+DA:163,644
+DA:167,644
+DA:169,5
+BRDA:169,0,0,5
+DA:170,5
+BRDA:170,1,0,-
+DA:171,0
+DA:175,644
+FNF:9
+FNH:6
+LF:55
+LH:42
+BRF:5
+BRH:1
+end_of_record
+TN:
+SF:src/HatsSignerGate.sol
+FN:125,HatsSignerGate._checkOwner
+FNDA:14975,HatsSignerGate._checkOwner
+DA:126,14975
+BRDA:126,0,0,2366
+FN:130,HatsSignerGate._checkUnlocked
+FNDA:17754,HatsSignerGate._checkUnlocked
+DA:131,2829
+BRDA:131,1,0,2829
+FN:138,HatsSignerGate.
+FNDA:269,HatsSignerGate.
+DA:145,269
+DA:146,269
+DA:147,269
+DA:148,269
+DA:149,269
+DA:152,269
+FN:160,HatsSignerGate.setUp
+FNDA:714,HatsSignerGate.setUp
+DA:161,712
+DA:164,712
+BRDA:164,2,0,450
+DA:165,450
+DA:171,712
+DA:173,264
+BRDA:173,3,0,264
+DA:176,712
+DA:179,712
+DA:180,712
+DA:181,712
+DA:184,712
+DA:187,712
+DA:188,712
+DA:189,1545
+DA:193,712
+BRDA:193,4,0,515
+DA:196,712
+DA:197,712
+DA:198,712
+FN:206,HatsSignerGate.claimSigner
+FNDA:12194,HatsSignerGate.claimSigner
+DA:208,12194
+DA:211,11680
+FN:215,HatsSignerGate.claimSignerFor
+FNDA:2313,HatsSignerGate.claimSignerFor
+DA:217,2313
+BRDA:217,5,0,257
+DA:220,2056
+DA:223,1285
+FN:227,HatsSignerGate.claimSignersFor
+FNDA:2056,HatsSignerGate.claimSignersFor
+DA:229,2056
+BRDA:229,6,0,514
+DA:232,1542
+DA:233,1542
+BRDA:233,7,0,-
+DA:235,1542
+DA:237,1542
+DA:239,1542
+DA:242,1542
+DA:245,1542
+DA:248,1542
+DA:249,12116
+DA:250,12116
+DA:253,12116
+DA:256,11602
+BRDA:256,8,0,6278
+DA:258,6278
+DA:261,6278
+BRDA:261,9,0,451
+BRDA:261,9,1,5827
+DA:262,451
+DA:265,5827
+DA:266,5827
+DA:270,6278
+DA:275,1028
+DA:276,1028
+BRDA:276,10,0,745
+DA:277,745
+FN:282,HatsSignerGate.removeSigner
+FNDA:772,HatsSignerGate.removeSigner
+DA:283,772
+BRDA:283,11,0,257
+DA:286,515
+FN:294,HatsSignerGate.lock
+FNDA:3087,HatsSignerGate.lock
+DA:295,3087
+DA:296,2830
+DA:297,2829
+FN:301,HatsSignerGate.setOwnerHat
+FNDA:771,HatsSignerGate.setOwnerHat
+DA:302,771
+DA:303,514
+DA:304,257
+FN:308,HatsSignerGate.addSignerHats
+FNDA:771,HatsSignerGate.addSignerHats
+DA:309,771
+DA:310,514
+DA:311,257
+FN:315,HatsSignerGate.setThresholdConfig
+FNDA:776,HatsSignerGate.setThresholdConfig
+DA:316,776
+DA:317,519
+DA:318,262
+DA:321,261
+DA:324,261
+DA:326,261
+BRDA:326,12,0,203
+DA:327,203
+DA:330,261
+FN:334,HatsSignerGate.setClaimableFor
+FNDA:4369,HatsSignerGate.setClaimableFor
+DA:335,4369
+DA:336,4112
+DA:337,3855
+FN:341,HatsSignerGate.detachHSG
+FNDA:259,HatsSignerGate.detachHSG
+DA:342,259
+DA:343,2
+DA:344,1
+DA:347,1
+DA:348,1
+DA:349,1
+FN:353,HatsSignerGate.migrateToNewHSG
+FNDA:517,HatsSignerGate.migrateToNewHSG
+DA:356,517
+DA:357,516
+DA:359,515
+DA:361,515
+DA:363,515
+DA:365,515
+DA:368,515
+DA:369,515
+BRDA:369,13,0,514
+DA:371,514
+BRDA:371,14,0,-
+DA:373,514
+DA:375,258
+FN:379,HatsSignerGate.enableDelegatecallTarget
+FNDA:1777,HatsSignerGate.enableDelegatecallTarget
+DA:380,1777
+DA:381,1520
+DA:383,1263
+FN:387,HatsSignerGate.disableDelegatecallTarget
+FNDA:771,HatsSignerGate.disableDelegatecallTarget
+DA:388,771
+DA:389,514
+DA:391,257
+FN:400,HatsSignerGate.checkTransaction
+FNDA:41,HatsSignerGate.checkTransaction
+DA:414,817
+BRDA:414,15,0,1
+DA:420,816
+BRDA:420,16,0,-
+DA:423,816
+BRDA:423,17,0,812
+DA:424,812
+DA:428,816
+DA:437,816
+BRDA:437,18,0,2
+DA:440,814
+BRDA:440,19,0,7
+DA:441,7
+DA:458,811
+DA:459,811
+DA:462,811
+DA:464,811
+BRDA:464,20,0,251
+BRDA:464,20,1,558
+DA:467,251
+BRDA:467,21,0,3
+DA:471,248
+DA:472,248
+DA:473,248
+DA:474,560
+BRDA:474,22,0,2
+DA:478,2
+DA:487,806
+BRDA:487,23,0,258
+DA:490,548
+DA:505,548
+BRDA:505,24,0,261
+FN:520,HatsSignerGate.checkAfterExecution
+FNDA:27,HatsSignerGate.checkAfterExecution
+DA:522,541
+DA:523,261
+BRDA:523,25,0,261
+DA:529,282
+DA:530,12
+BRDA:530,26,0,12
+FN:539,HatsSignerGate.thresholdConfig
+FNDA:774,HatsSignerGate.thresholdConfig
+DA:540,774
+FN:544,HatsSignerGate.isValidSigner
+FNDA:10590,HatsSignerGate.isValidSigner
+DA:547,43404
+FN:551,HatsSignerGate.isValidSignerHat
+FNDA:15142,HatsSignerGate.isValidSignerHat
+DA:552,50058
+FN:556,HatsSignerGate.validSignerCount
+FNDA:2060,HatsSignerGate.validSignerCount
+DA:557,2060
+FN:561,HatsSignerGate.canAttachToSafe
+FNDA:258,HatsSignerGate.canAttachToSafe
+DA:562,258
+FN:566,HatsSignerGate.getSafeDeployParamAddresses
+FNDA:1,HatsSignerGate.getSafeDeployParamAddresses
+DA:576,1
+FN:585,HatsSignerGate._setOwnerHat
+FNDA:1226,HatsSignerGate._setOwnerHat
+DA:586,1226
+DA:587,1226
+FN:593,HatsSignerGate._addSignerHats
+FNDA:2256,HatsSignerGate._addSignerHats
+DA:594,2256
+DA:595,16352
+DA:598,2256
+FN:603,HatsSignerGate._setThresholdConfig
+FNDA:4056,HatsSignerGate._setThresholdConfig
+DA:605,4056
+BRDA:605,27,0,256
+DA:607,3800
+DA:608,2640
+BRDA:608,28,0,2640
+BRDA:607,28,1,903
+DA:609,2640
+BRDA:609,29,0,258
+DA:612,1160
+BRDA:612,30,0,257
+DA:615,3285
+DA:618,3285
+FN:625,HatsSignerGate._countValidSigners
+FNDA:2317,HatsSignerGate._countValidSigners
+DA:627,2317
+DA:628,20339
+DA:629,14577
+BRDA:629,31,0,14577
+DA:631,14577
+FN:644,HatsSignerGate._countValidSignatures
+FNDA:1576,HatsSignerGate._countValidSignatures
+DA:650,1576
+DA:651,1576
+DA:652,1576
+DA:653,1576
+DA:654,1576
+DA:656,13279
+DA:657,11703
+DA:658,11703
+DA:659,3717
+BRDA:659,32,0,3717
+BRDA:658,32,1,2683
+DA:661,3717
+DA:662,7986
+DA:663,2687
+BRDA:663,33,0,2687
+BRDA:662,33,1,2683
+DA:665,2687
+DA:666,5299
+DA:667,2616
+BRDA:667,34,0,2616
+BRDA:666,34,1,2683
+DA:670,2616
+DA:674,2683
+DA:677,11703
+DA:678,6414
+BRDA:678,35,0,6414
+DA:680,6414
+FN:688,HatsSignerGate._setClaimableFor
+FNDA:4824,HatsSignerGate._setClaimableFor
+DA:689,4824
+DA:690,4824
+FN:697,HatsSignerGate._registerSigner
+FNDA:34916,HatsSignerGate._registerSigner
+DA:699,34916
+BRDA:699,36,0,1028
+DA:702,33888
+BRDA:702,37,0,1028
+DA:706,32860
+DA:707,20923
+BRDA:707,38,0,20923
+BRDA:707,39,0,514
+DA:711,32346
+DA:714,32346
+FN:721,HatsSignerGate._addSigner
+FNDA:22571,HatsSignerGate._addSigner
+DA:722,22571
+DA:725,22571
+DA:726,20877
+BRDA:726,40,0,20877
+DA:727,20877
+DA:730,20877
+DA:733,20877
+DA:734,5164
+BRDA:734,41,0,5164
+BRDA:733,41,1,15713
+DA:735,5164
+DA:738,15713
+DA:740,15713
+DA:744,20877
+FN:751,HatsSignerGate._removeSigner
+FNDA:1286,HatsSignerGate._removeSigner
+DA:752,1286
+DA:753,1286
+DA:754,1286
+DA:756,1286
+DA:758,1286
+DA:759,835
+BRDA:759,42,0,835
+BRDA:758,42,1,451
+DA:760,835
+DA:763,451
+DA:765,451
+DA:770,1286
+FN:777,HatsSignerGate._getRequiredValidSignatures
+FNDA:28832,HatsSignerGate._getRequiredValidSignatures
+DA:779,28832
+DA:782,28832
+DA:783,26004
+BRDA:783,43,0,26004
+BRDA:782,43,1,2828
+DA:784,26004
+BRDA:784,44,0,2996
+BRDA:784,44,1,4318
+DA:785,23008
+BRDA:785,45,0,18690
+BRDA:785,45,1,4318
+DA:786,4318
+DA:790,2828
+DA:792,2828
+BRDA:792,46,0,1893
+FN:801,HatsSignerGate._getNewThreshold
+FNDA:26223,HatsSignerGate._getNewThreshold
+DA:803,26223
+DA:805,26223
+DA:806,3678
+BRDA:806,47,0,3678
+FN:811,HatsSignerGate._lock
+FNDA:3095,HatsSignerGate._lock
+DA:812,3095
+DA:813,3095
+FN:819,HatsSignerGate._setDelegatecallTarget
+FNDA:4425,HatsSignerGate._setDelegatecallTarget
+DA:820,4425
+DA:821,4425
+FN:825,HatsSignerGate.
+FNDA:0,HatsSignerGate.
+FN:844,HatsSignerGate.execTransactionFromModule
+FNDA:16,HatsSignerGate.execTransactionFromModule
+DA:851,15
+DA:854,10
+DA:857,10
+FN:870,HatsSignerGate.execTransactionFromModuleReturnData
+FNDA:16,HatsSignerGate.execTransactionFromModuleReturnData
+DA:877,15
+DA:880,10
+DA:883,10
+FN:888,HatsSignerGate.disableModule
+FNDA:1028,HatsSignerGate.disableModule
+DA:889,1028
+DA:890,771
+DA:891,514
+FN:897,HatsSignerGate.enableModule
+FNDA:2077,HatsSignerGate.enableModule
+DA:898,2077
+DA:899,1820
+DA:900,1563
+FN:910,HatsSignerGate.setGuard
+FNDA:1549,HatsSignerGate.setGuard
+DA:911,1549
+DA:912,1292
+DA:913,1035
+FN:927,HatsSignerGate._checkModuleTransaction
+FNDA:802,HatsSignerGate._checkModuleTransaction
+DA:929,802
+DA:930,536
+BRDA:930,48,0,536
+BRDA:929,48,1,261
+DA:932,536
+BRDA:932,49,0,263
+DA:936,273
+DA:937,273
+DA:938,273
+DA:939,266
+DA:940,5
+BRDA:940,50,0,5
+DA:943,5
+FN:953,HatsSignerGate._checkSafeState
+FNDA:289,HatsSignerGate._checkSafeState
+DA:954,289
+BRDA:954,51,0,4
+DA:957,285
+BRDA:957,52,0,7
+DA:960,278
+BRDA:960,53,0,10
+DA:963,268
+BRDA:963,54,0,3
+DA:966,265
+DA:973,265
+DA:974,261
+BRDA:974,55,0,261
+FN:983,HatsSignerGate._beforeExecTransactionFromModule
+FNDA:30,HatsSignerGate._beforeExecTransactionFromModule
+DA:989,30
+BRDA:989,56,0,-
+DA:992,30
+DA:994,30
+DA:997,30
+FN:1005,HatsSignerGate._afterExecTransactionFromModule
+FNDA:20,HatsSignerGate._afterExecTransactionFromModule
+DA:1008,18
+BRDA:1008,57,0,18
+BRDA:1007,57,1,2
+DA:1010,2
+DA:1015,20
+BRDA:1015,58,0,16
+DA:1018,4
+FNF:48
+FNH:47
+LF:247
+LH:247
+BRF:72
+BRH:68
+end_of_record
+TN:
+SF:src/lib/SafeManagerLib.sol
+FN:38,SafeManagerLib.deploySafeAndAttachHSG
+FNDA:451,SafeManagerLib.deploySafeAndAttachHSG
+DA:44,451
+DA:47,451
+DA:48,451
+DA:50,451
+DA:65,451
+DA:68,451
+DA:69,451
+DA:72,451
+FN:89,SafeManagerLib.encodeEnableModuleAction
+FNDA:1224,SafeManagerLib.encodeEnableModuleAction
+DA:90,1224
+FN:95,SafeManagerLib.encodeDisableModuleAction
+FNDA:1031,SafeManagerLib.encodeDisableModuleAction
+DA:100,1031
+FN:108,SafeManagerLib.encodeSetGuardAction
+FNDA:1742,SafeManagerLib.encodeSetGuardAction
+DA:109,1742
+FN:113,SafeManagerLib.encodeRemoveHSGAsGuardAction
+FNDA:1,SafeManagerLib.encodeRemoveHSGAsGuardAction
+DA:115,1
+FN:123,SafeManagerLib.encodeSwapOwnerAction
+FNDA:6707,SafeManagerLib.encodeSwapOwnerAction
+DA:128,6707
+FN:133,SafeManagerLib.encodeRemoveOwnerAction
+FNDA:708,SafeManagerLib.encodeRemoveOwnerAction
+DA:138,708
+FN:142,SafeManagerLib.encodeAddOwnerWithThresholdAction
+FNDA:21797,SafeManagerLib.encodeAddOwnerWithThresholdAction
+DA:147,21797
+FN:155,SafeManagerLib.encodeChangeThresholdAction
+FNDA:1777,SafeManagerLib.encodeChangeThresholdAction
+DA:156,1777
+FN:164,SafeManagerLib.execSafeTransactionFromHSG
+FNDA:32284,SafeManagerLib.execSafeTransactionFromHSG
+DA:165,32284
+FN:169,SafeManagerLib.execDisableHSGAsOnlyModule
+FNDA:2,SafeManagerLib.execDisableHSGAsOnlyModule
+DA:171,2
+DA:174,2
+FN:179,SafeManagerLib.execDisableHSGAsModule
+FNDA:772,SafeManagerLib.execDisableHSGAsModule
+DA:180,772
+DA:182,772
+FN:187,SafeManagerLib.execRemoveHSGAsGuard
+FNDA:517,SafeManagerLib.execRemoveHSGAsGuard
+DA:188,517
+DA:190,517
+FN:196,SafeManagerLib.execAttachNewHSG
+FNDA:516,SafeManagerLib.execAttachNewHSG
+DA:197,516
+DA:198,516
+DA:200,516
+DA:201,516
+FN:205,SafeManagerLib.execChangeThreshold
+FNDA:1520,SafeManagerLib.execChangeThreshold
+DA:206,1520
+FN:214,SafeManagerLib.getSafeGuard
+FNDA:294,SafeManagerLib.getSafeGuard
+DA:215,294
+FN:219,SafeManagerLib.getSafeFallbackHandler
+FNDA:1356,SafeManagerLib.getSafeFallbackHandler
+DA:221,1356
+FN:227,SafeManagerLib.getModulesWith1
+FNDA:266,SafeManagerLib.getModulesWith1
+DA:228,266
+FN:233,SafeManagerLib.canAttachHSG
+FNDA:260,SafeManagerLib.canAttachHSG
+DA:234,260
+DA:236,260
+FN:244,SafeManagerLib.findPrevOwner
+FNDA:552,SafeManagerLib.findPrevOwner
+DA:245,552
+DA:247,552
+DA:248,4546
+DA:249,552
+BRDA:249,0,0,552
+DA:250,470
+FNF:20
+FNH:20
+LF:38
+LH:38
+BRF:1
+BRH:1
+end_of_record
+TN:
+SF:src/lib/zodiac-modified/GuardableUnowned.sol
+FN:23,GuardableUnowned._setGuard
+FNDA:1550,GuardableUnowned._setGuard
+DA:24,1550
+BRDA:24,0,0,1293
+DA:25,1293
+BRDA:25,1,0,-
+DA:26,0
+DA:29,1293
+DA:30,1293
+FN:33,GuardableUnowned.getGuard
+FNDA:1805,GuardableUnowned.getGuard
+DA:34,1805
+FNF:2
+FNH:2
+LF:6
+LH:5
+BRF:2
+BRH:1
+end_of_record
+TN:
+SF:src/lib/zodiac-modified/ModifierUnowned.sol
+FN:75,ModifierUnowned.moduleOnly
+FNDA:16,ModifierUnowned.moduleOnly
+DA:76,16
+BRDA:76,0,0,1
+FN:84,ModifierUnowned.disableModule
+FNDA:514,ModifierUnowned.disableModule
+DA:85,514
+BRDA:85,1,0,-
+DA:86,0
+DA:88,514
+BRDA:88,2,0,-
+DA:89,514
+DA:90,514
+DA:91,514
+FN:97,ModifierUnowned._enableModule
+FNDA:3108,ModifierUnowned._enableModule
+DA:98,3108
+BRDA:98,3,0,-
+DA:99,0
+DA:101,3108
+BRDA:101,4,0,-
+DA:102,3108
+DA:103,3108
+DA:104,3108
+FN:109,ModifierUnowned.isModuleEnabled
+FNDA:1799,ModifierUnowned.isModuleEnabled
+DA:110,1799
+FN:120,ModifierUnowned.getModulesPaginated
+FNDA:514,ModifierUnowned.getModulesPaginated
+DA:125,514
+BRDA:125,5,0,-
+DA:126,0
+DA:128,514
+BRDA:128,6,0,-
+DA:129,0
+DA:133,514
+DA:136,514
+DA:137,514
+DA:138,2056
+DA:139,1542
+DA:140,1542
+DA:141,1542
+DA:151,514
+BRDA:151,7,0,-
+DA:152,0
+DA:157,514
+FN:163,ModifierUnowned.setupModules
+FNDA:712,ModifierUnowned.setupModules
+DA:164,712
+BRDA:164,8,0,-
+DA:165,0
+DA:167,712
+FNF:6
+FNH:6
+LF:31
+LH:25
+BRF:9
+BRH:1
+end_of_record
+TN:
+SF:test/TestSuite.t.sol
+FN:27,SafeTestHelpers._getEthTransferSafeTxHash
+FNDA:0,SafeTestHelpers._getEthTransferSafeTxHash
+DA:28,0
+FN:43,SafeTestHelpers._getTxHash
+FNDA:36,SafeTestHelpers._getTxHash
+DA:48,36
+FN:63,SafeTestHelpers._createNSigsForTx
+FNDA:42,SafeTestHelpers._createNSigsForTx
+DA:64,42
+DA:65,42
+DA:66,42
+DA:67,42
+DA:68,42
+DA:70,42
+DA:72,89
+DA:74,89
+DA:76,89
+DA:77,89
+DA:79,42
+DA:81,42
+DA:82,89
+DA:84,89
+FN:88,SafeTestHelpers._signaturesForEthTransferTx
+FNDA:0,SafeTestHelpers._signaturesForEthTransferTx
+DA:93,0
+DA:96,0
+DA:97,0
+DA:98,0
+DA:99,0
+DA:100,0
+DA:102,0
+DA:104,0
+DA:106,0
+DA:108,0
+DA:109,0
+DA:111,0
+DA:115,0
+DA:118,0
+DA:119,0
+DA:120,0
+FN:124,SafeTestHelpers._createAddressesFromPks
+FNDA:200,SafeTestHelpers._createAddressesFromPks
+DA:129,200
+DA:130,200
+DA:132,200
+DA:133,4000
+DA:134,4000
+FN:139,SafeTestHelpers._sort
+FNDA:42,SafeTestHelpers._sort
+DA:140,42
+DA:141,42
+DA:142,42
+DA:143,41
+DA:144,88
+DA:145,47
+DA:146,47
+DA:147,47
+BRDA:147,1,0,47
+DA:148,47
+DA:149,47
+DA:150,47
+DA:153,41
+BRDA:153,2,0,-
+DA:154,41
+BRDA:154,3,0,-
+FN:157,SafeTestHelpers._findPrevOwner
+FNDA:4,SafeTestHelpers._findPrevOwner
+DA:158,4
+DA:160,4
+DA:161,4
+BRDA:161,4,0,4
+DA:162,4
+DA:163,0
+FN:169,SafeTestHelpers._getSafeTxHash
+FNDA:0,SafeTestHelpers._getSafeTxHash
+DA:170,9
+FN:185,SafeTestHelpers._getSafeDelegatecallHash
+FNDA:0,SafeTestHelpers._getSafeDelegatecallHash
+DA:190,0
+FN:196,SafeTestHelpers._executeSafeTxFrom
+FNDA:0,SafeTestHelpers._executeSafeTxFrom
+DA:197,0
+FN:213,SafeTestHelpers._executeEthTransferFromSafe
+FNDA:0,SafeTestHelpers._executeEthTransferFromSafe
+DA:214,0
+DA:216,0
+DA:218,0
+FN:291,TestSuite.setUp
+FNDA:200,TestSuite.setUp
+DA:293,200
+DA:296,200
+DA:297,200
+DA:298,200
+DA:301,200
+DA:302,200
+DA:303,200
+DA:304,200
+DA:305,200
+DA:306,200
+DA:309,200
+DA:312,200
+DA:315,200
+DA:316,200
+DA:317,200
+DA:318,200
+DA:319,200
+DA:322,200
+DA:323,200
+DA:324,200
+DA:325,200
+DA:328,200
+DA:329,200
+DA:330,200
+DA:331,200
+DA:334,200
+DA:335,200
+DA:337,200
+DA:338,200
+DA:339,200
+DA:341,200
+DA:342,2000
+DA:346,200
+DA:347,200
+DA:349,200
+DA:352,200
+FN:363,TestSuite._deploySafe
+FNDA:516,TestSuite._deploySafe
+DA:365,516
+DA:378,516
+FN:381,TestSuite._deployHSG
+FNDA:257,TestSuite._deployHSG
+DA:394,257
+DA:395,257
+DA:406,257
+DA:408,257
+BRDA:408,0,0,-
+DA:409,0
+DA:413,257
+FN:416,TestSuite._deployHSGAndSafe
+FNDA:382,TestSuite._deployHSGAndSafe
+DA:427,382
+DA:428,382
+DA:439,382
+DA:440,382
+DA:441,382
+FN:444,TestSuite._getSafeGuard
+FNDA:1033,TestSuite._getSafeGuard
+DA:445,1033
+FN:452,TestSuite._addSignersSameHat
+FNDA:1061,TestSuite._addSignersSameHat
+DA:453,1061
+DA:454,8216
+DA:455,8216
+DA:456,8216
+DA:457,8216
+DA:458,8216
+FN:462,TestSuite._addSignersDifferentHats
+FNDA:2,TestSuite._addSignersDifferentHats
+DA:463,2
+DA:464,6
+DA:465,6
+DA:466,6
+FN:470,TestSuite._setSignerValidity
+FNDA:53489,TestSuite._setSignerValidity
+DA:471,40360
+BRDA:471,1,0,40360
+BRDA:471,1,1,13129
+DA:472,40360
+DA:474,40012
+DA:475,40012
+DA:478,13129
+DA:479,13129
+FN:484,TestSuite._constructSingleActionMultiSendTx
+FNDA:17,TestSuite._constructSingleActionMultiSendTx
+DA:489,17
+DA:496,17
+DA:497,17
+FN:504,TestSuite._generateFuzzingAddresses
+FNDA:200,TestSuite._generateFuzzingAddresses
+DA:505,200
+DA:506,200
+DA:507,10000
+DA:509,200
+FN:516,TestSuite.assertValidSignerHats
+FNDA:0,TestSuite.assertValidSignerHats
+DA:517,771
+DA:518,8994
+FN:522,TestSuite.assertCorrectModules
+FNDA:0,TestSuite.assertCorrectModules
+DA:523,514
+DA:524,514
+DA:525,514
+DA:527,1542
+DA:529,514
+FN:532,TestSuite.assertEq
+FNDA:0,TestSuite.assertEq
+DA:536,514
+DA:537,514
+DA:538,514
+FN:541,TestSuite.assertOnlyModule
+FNDA:0,TestSuite.assertOnlyModule
+DA:542,0
+DA:543,0
+DA:544,0
+DA:545,0
+FN:552,TestSuite._createValidThresholdConfig
+FNDA:2827,TestSuite._createValidThresholdConfig
+DA:558,2827
+DA:560,2827
+DA:561,2827
+BRDA:561,3,0,1720
+BRDA:561,3,1,1107
+DA:563,1720
+DA:566,1107
+DA:569,2827
+DA:570,2827
+DA:571,2827
+DA:573,2827
+FN:576,TestSuite._calcProportionalTargetSignatures
+FNDA:206,TestSuite._calcProportionalTargetSignatures
+DA:577,206
+FN:581,TestSuite._calcProportionalRequiredValidSignatures
+FNDA:257,TestSuite._calcProportionalRequiredValidSignatures
+DA:586,257
+DA:587,206
+DA:588,206
+DA:589,160
+FN:592,TestSuite._calcAbsoluteRequiredValidSignatures
+FNDA:514,TestSuite._calcAbsoluteRequiredValidSignatures
+DA:597,514
+DA:598,463
+DA:599,354
+FN:602,TestSuite._calcRequiredValidSignatures
+FNDA:0,TestSuite._calcRequiredValidSignatures
+DA:607,0
+BRDA:607,8,0,-
+DA:608,0
+DA:610,0
+FN:615,TestSuite._mockHatWearer
+FNDA:2313,TestSuite._mockHatWearer
+DA:616,2313
+FN:621,TestSuite._getRandomBool
+FNDA:10606,TestSuite._getRandomBool
+DA:622,10606
+FN:626,TestSuite._getRandomSignerHats
+FNDA:1028,TestSuite._getRandomSignerHats
+DA:628,1028
+DA:631,1028
+DA:632,1028
+DA:633,15346
+DA:635,1028
+FN:639,TestSuite._getRandomValidSignerHatsWithoutReplacement
+FNDA:0,TestSuite._getRandomValidSignerHatsWithoutReplacement
+DA:644,0
+DA:645,0
+DA:646,0
+DA:647,0
+DA:649,0
+DA:650,0
+DA:651,0
+BRDA:651,9,0,-
+DA:652,0
+DA:653,0
+DA:654,0
+DA:658,0
+FN:662,TestSuite._getRandomValidSignerHatsWithReplacement
+FNDA:0,TestSuite._getRandomValidSignerHatsWithReplacement
+DA:667,0
+DA:668,0
+DA:669,0
+DA:670,0
+DA:672,0
+FN:675,TestSuite._getRandomValidSignerHat
+FNDA:3953,TestSuite._getRandomValidSignerHat
+DA:676,3953
+DA:677,3953
+FN:680,TestSuite._getRandomAddress
+FNDA:8769,TestSuite._getRandomAddress
+DA:681,8769
+FN:684,TestSuite._getRandomAddress
+FNDA:18376,TestSuite._getRandomAddress
+DA:685,18376
+DA:686,18376
+FN:689,TestSuite._getRandomSigners
+FNDA:0,TestSuite._getRandomSigners
+DA:690,0
+DA:691,0
+DA:692,0
+DA:693,0
+DA:695,0
+DA:696,0
+DA:697,0
+BRDA:697,10,0,-
+DA:698,0
+DA:699,0
+DA:700,0
+DA:704,0
+FN:707,TestSuite._getRandomAddresses
+FNDA:0,TestSuite._getRandomAddresses
+DA:708,0
+BRDA:708,11,0,-
+BRDA:708,11,1,-
+DA:709,0
+DA:710,0
+DA:711,0
+DA:713,0
+DA:714,0
+DA:715,0
+BRDA:715,12,0,-
+DA:716,0
+DA:717,0
+DA:718,0
+DA:722,0
+FN:725,TestSuite._getRandomGuard
+FNDA:771,TestSuite._getRandomGuard
+DA:726,771
+DA:727,771
+FN:730,TestSuite._getSignaturesForEmptyTx
+FNDA:9,TestSuite._getSignaturesForEmptyTx
+DA:735,9
+DA:736,9
+BRDA:736,13,0,9
+BRDA:736,13,1,-
+DA:737,9
+DA:739,0
+DA:741,9
+FN:744,TestSuite._executeEmptyCallFromSafe
+FNDA:9,TestSuite._executeEmptyCallFromSafe
+DA:746,9
+DA:749,9
+DA:750,9
+FN:753,TestSuite._createContractSignature
+FNDA:1030,TestSuite._createContractSignature
+DA:755,1030
+DA:757,1030
+DA:759,1030
+DA:760,1030
+FN:763,TestSuite._createNContractSigs
+FNDA:515,TestSuite._createNContractSigs
+DA:764,515
+DA:765,1030
+DA:767,0
+FN:770,TestSuite.callerIsOwner
+FNDA:2572,TestSuite.callerIsOwner
+DA:772,2572
+DA:775,2572
+FN:780,TestSuite.callerIsSafe
+FNDA:258,TestSuite.callerIsSafe
+DA:781,257
+BRDA:781,14,0,257
+BRDA:781,14,1,-
+DA:782,257
+DA:784,1
+FN:791,WithHSGInstanceTest.setUp
+FNDA:124,WithHSGInstanceTest.setUp
+DA:792,124
+DA:794,124
+FN:806,WithHSGInstanceTest.isLocked
+FNDA:2572,WithHSGInstanceTest.isLocked
+DA:807,2572
+BRDA:807,0,0,514
+DA:808,514
+DA:809,514
+FN:814,WithHSGInstanceTest.isClaimableFor
+FNDA:257,WithHSGInstanceTest.isClaimableFor
+DA:815,257
+BRDA:815,1,0,257
+DA:816,257
+DA:817,257
+FN:829,WithHSGHarnessInstanceTest.setUp
+FNDA:68,WithHSGHarnessInstanceTest.setUp
+DA:830,68
+DA:833,68
+DA:842,68
+DA:855,68
+DA:863,68
+FN:866,WithHSGHarnessInstanceTest._addSignersSameHat
+FNDA:9,WithHSGHarnessInstanceTest._addSignersSameHat
+DA:867,9
+DA:868,18
+DA:869,18
+DA:870,18
+DA:871,18
+DA:872,18
+FN:877,WithHSGHarnessInstanceTest._addRandomSigners
+FNDA:771,WithHSGHarnessInstanceTest._addRandomSigners
+DA:879,771
+DA:882,771
+DA:883,771
+DA:885,9856
+DA:888,9856
+DA:889,9856
+DA:890,102228
+BRDA:890,0,0,2049
+DA:891,2049
+DA:892,2049
+DA:895,9856
+BRDA:895,1,0,7807
+DA:896,7807
+DA:899,7807
+DA:900,7807
+DA:902,7807
+DA:903,7807
+DA:906,7807
+DA:907,7807
+FN:918,WithHSGHarnessInstanceTest._generateUniqueECDSASignatures
+FNDA:514,WithHSGHarnessInstanceTest._generateUniqueECDSASignatures
+DA:924,514
+DA:925,514
+DA:926,514
+DA:928,514
+DA:930,5232
+DA:932,9892
+DA:936,5232
+DA:939,5232
+DA:940,5232
+DA:943,5232
+DA:946,5232
+DA:949,5232
+DA:950,5232
+DA:953,5232
+DA:954,5232
+DA:957,5232
+DA:960,5232
+DA:961,5232
+DA:962,2750
+BRDA:962,2,0,2750
+DA:963,2750
+DA:964,2750
+FN:974,WithHSGHarnessInstanceTest._generateUniqueNonECDSASignatures
+FNDA:514,WithHSGHarnessInstanceTest._generateUniqueNonECDSASignatures
+DA:978,514
+DA:979,514
+DA:980,514
+DA:982,514
+DA:984,5374
+DA:986,10100
+DA:990,5374
+DA:993,5374
+DA:994,5374
+DA:997,5374
+DA:1000,5374
+DA:1001,5374
+DA:1004,5374
+DA:1005,5374
+DA:1008,5374
+DA:1011,5374
+DA:1012,5374
+DA:1013,2828
+BRDA:1013,3,0,2828
+DA:1014,2828
+DA:1015,2828
+FN:1020,WithHSGHarnessInstanceTest._assertTransientStateVariables
+FNDA:776,WithHSGHarnessInstanceTest._assertTransientStateVariables
+DA:1029,776
+DA:1030,776
+DA:1031,776
+DA:1032,776
+DA:1033,776
+BRDA:1033,4,0,1
+BRDA:1033,4,1,775
+DA:1034,1
+DA:1035,1
+DA:1036,1
+DA:1038,775
+DA:1039,775
+DA:1040,775
+FN:1046,WithHSGHarnessInstanceTest.assertCorrectTransientState
+FNDA:257,WithHSGHarnessInstanceTest.assertCorrectTransientState
+DA:1051,257
+DA:1052,257
+DA:1053,257
+FNF:57
+FNH:42
+LF:331
+LH:260
+BRF:27
+BRH:16
+end_of_record
+TN:
+SF:test/harnesses/HatsSignerGateHarness.sol
+FN:18,HatsSignerGateHarness.
+FNDA:69,HatsSignerGateHarness.
+FN:42,HatsSignerGateHarness.setExistingOwnersHash
+FNDA:258,HatsSignerGateHarness.setExistingOwnersHash
+DA:43,258
+FN:46,HatsSignerGateHarness.setExistingThreshold
+FNDA:259,HatsSignerGateHarness.setExistingThreshold
+DA:47,259
+FN:50,HatsSignerGateHarness.setExistingFallbackHandler
+FNDA:258,HatsSignerGateHarness.setExistingFallbackHandler
+DA:51,258
+FN:58,HatsSignerGateHarness.exposed_checkOwner
+FNDA:51,HatsSignerGateHarness.exposed_checkOwner
+DA:59,51
+FN:62,HatsSignerGateHarness.exposed_checkUnlocked
+FNDA:2,HatsSignerGateHarness.exposed_checkUnlocked
+DA:63,2
+FN:66,HatsSignerGateHarness.exposed_lock
+FNDA:2,HatsSignerGateHarness.exposed_lock
+DA:67,2
+FN:70,HatsSignerGateHarness.exposed_setDelegatecallTarget
+FNDA:769,HatsSignerGateHarness.exposed_setDelegatecallTarget
+DA:71,769
+FN:74,HatsSignerGateHarness.exposed_setClaimableFor
+FNDA:257,HatsSignerGateHarness.exposed_setClaimableFor
+DA:75,257
+FN:78,HatsSignerGateHarness.exposed_registerSigner
+FNDA:8550,HatsSignerGateHarness.exposed_registerSigner
+DA:79,8550
+FN:82,HatsSignerGateHarness.exposed_addSigner
+FNDA:9606,HatsSignerGateHarness.exposed_addSigner
+DA:83,9606
+FN:86,HatsSignerGateHarness.exposed_removeSigner
+FNDA:771,HatsSignerGateHarness.exposed_removeSigner
+DA:87,771
+FN:90,HatsSignerGateHarness.exposed_setOwnerHat
+FNDA:257,HatsSignerGateHarness.exposed_setOwnerHat
+DA:91,257
+FN:94,HatsSignerGateHarness.exposed_addSignerHats
+FNDA:1287,HatsSignerGateHarness.exposed_addSignerHats
+DA:95,1287
+FN:98,HatsSignerGateHarness.exposed_setThresholdConfig
+FNDA:3339,HatsSignerGateHarness.exposed_setThresholdConfig
+DA:99,3082
+FN:102,HatsSignerGateHarness.exposed_countValidSigners
+FNDA:257,HatsSignerGateHarness.exposed_countValidSigners
+DA:103,257
+FN:106,HatsSignerGateHarness.exposed_countValidSignatures
+FNDA:1028,HatsSignerGateHarness.exposed_countValidSignatures
+DA:111,1028
+FN:114,HatsSignerGateHarness.exposed_checkModuleTransaction
+FNDA:772,HatsSignerGateHarness.exposed_checkModuleTransaction
+DA:115,772
+FN:118,HatsSignerGateHarness.exposed_checkSafeState
+FNDA:261,HatsSignerGateHarness.exposed_checkSafeState
+DA:119,261
+FN:122,HatsSignerGateHarness.exposed_enableModule
+FNDA:0,HatsSignerGateHarness.exposed_enableModule
+DA:123,0
+FN:126,HatsSignerGateHarness.exposed_setGuard
+FNDA:0,HatsSignerGateHarness.exposed_setGuard
+DA:127,0
+FN:130,HatsSignerGateHarness.exposed_getRequiredValidSignatures
+FNDA:1542,HatsSignerGateHarness.exposed_getRequiredValidSignatures
+DA:131,1542
+FN:134,HatsSignerGateHarness.exposed_getNewThreshold
+FNDA:9031,HatsSignerGateHarness.exposed_getNewThreshold
+DA:135,9031
+FN:138,HatsSignerGateHarness.exposed_existingOwnersHash
+FNDA:257,HatsSignerGateHarness.exposed_existingOwnersHash
+DA:139,257
+FN:142,HatsSignerGateHarness.exposed_existingThreshold
+FNDA:257,HatsSignerGateHarness.exposed_existingThreshold
+DA:143,257
+FN:146,HatsSignerGateHarness.exposed_existingFallbackHandler
+FNDA:257,HatsSignerGateHarness.exposed_existingFallbackHandler
+DA:147,257
+FN:150,HatsSignerGateHarness.exposed_operation
+FNDA:0,HatsSignerGateHarness.exposed_operation
+DA:151,0
+FN:154,HatsSignerGateHarness.exposed_reentrancyGuard
+FNDA:0,HatsSignerGateHarness.exposed_reentrancyGuard
+DA:155,0
+FN:158,HatsSignerGateHarness.exposed_initialNonce
+FNDA:0,HatsSignerGateHarness.exposed_initialNonce
+DA:159,0
+FN:162,HatsSignerGateHarness.exposed_entrancyCounter
+FNDA:0,HatsSignerGateHarness.exposed_entrancyCounter
+DA:163,0
+FN:167,HatsSignerGateHarness.exposed_checkTransaction
+FNDA:776,HatsSignerGateHarness.exposed_checkTransaction
+DA:180,776
+DA:183,258
+DA:184,258
+DA:185,258
+DA:186,258
+DA:187,258
+DA:188,258
+DA:189,258
+FN:193,HatsSignerGateHarness.exposed_checkAfterExecution
+FNDA:514,HatsSignerGateHarness.exposed_checkAfterExecution
+DA:194,514
+FNF:32
+FNH:26
+LF:38
+LH:32
+BRF:0
+BRH:0
+end_of_record
+TN:
+SF:test/harnesses/SafeManagerLibHarness.sol
+FN:14,SafeManagerLibHarness.deploySafeAndAttachHSG
+FNDA:1,SafeManagerLibHarness.deploySafeAndAttachHSG
+DA:20,1
+FN:25,SafeManagerLibHarness.encodeEnableModuleAction
+FNDA:0,SafeManagerLibHarness.encodeEnableModuleAction
+DA:26,0
+FN:29,SafeManagerLibHarness.encodeDisableModuleAction
+FNDA:0,SafeManagerLibHarness.encodeDisableModuleAction
+DA:34,0
+FN:37,SafeManagerLibHarness.encodeSetGuardAction
+FNDA:0,SafeManagerLibHarness.encodeSetGuardAction
+DA:38,0
+FN:41,SafeManagerLibHarness.encodeRemoveHSGAsGuardAction
+FNDA:0,SafeManagerLibHarness.encodeRemoveHSGAsGuardAction
+DA:42,0
+FN:45,SafeManagerLibHarness.encodeSwapOwnerAction
+FNDA:0,SafeManagerLibHarness.encodeSwapOwnerAction
+DA:50,0
+FN:53,SafeManagerLibHarness.encodeRemoveOwnerAction
+FNDA:0,SafeManagerLibHarness.encodeRemoveOwnerAction
+DA:58,0
+FN:61,SafeManagerLibHarness.encodeAddOwnerWithThresholdAction
+FNDA:0,SafeManagerLibHarness.encodeAddOwnerWithThresholdAction
+DA:62,0
+FN:65,SafeManagerLibHarness.encodeChangeThresholdAction
+FNDA:0,SafeManagerLibHarness.encodeChangeThresholdAction
+DA:66,0
+FN:69,SafeManagerLibHarness.execSafeTransactionFromHSG
+FNDA:0,SafeManagerLibHarness.execSafeTransactionFromHSG
+DA:70,0
+FN:73,SafeManagerLibHarness.execDisableHSGAsOnlyModule
+FNDA:1,SafeManagerLibHarness.execDisableHSGAsOnlyModule
+DA:74,1
+FN:77,SafeManagerLibHarness.execDisableHSGAsModule
+FNDA:257,SafeManagerLibHarness.execDisableHSGAsModule
+DA:78,257
+FN:81,SafeManagerLibHarness.execRemoveHSGAsGuard
+FNDA:1,SafeManagerLibHarness.execRemoveHSGAsGuard
+DA:82,1
+FN:85,SafeManagerLibHarness.execAttachNewHSG
+FNDA:1,SafeManagerLibHarness.execAttachNewHSG
+DA:86,1
+FN:89,SafeManagerLibHarness.execChangeThreshold
+FNDA:514,SafeManagerLibHarness.execChangeThreshold
+DA:90,514
+FN:93,SafeManagerLibHarness.getSafeGuard
+FNDA:0,SafeManagerLibHarness.getSafeGuard
+DA:94,0
+FN:97,SafeManagerLibHarness.getSafeFallbackHandler
+FNDA:0,SafeManagerLibHarness.getSafeFallbackHandler
+DA:98,0
+FN:101,SafeManagerLibHarness.getModulesWith1
+FNDA:0,SafeManagerLibHarness.getModulesWith1
+DA:102,0
+FN:105,SafeManagerLibHarness.canAttachHSG
+FNDA:0,SafeManagerLibHarness.canAttachHSG
+DA:106,0
+FN:109,SafeManagerLibHarness.findPrevOwner
+FNDA:0,SafeManagerLibHarness.findPrevOwner
+DA:110,0
+FNF:20
+FNH:6
+LF:20
+LH:6
+BRF:0
+BRH:0
+end_of_record
+TN:
+SF:test/mocks/TestGuard.sol
+FN:20,TestGuard.
+FNDA:600,TestGuard.
+DA:21,600
+DA:22,600
+FN:25,TestGuard.setModule
+FNDA:0,TestGuard.setModule
+DA:26,0
+FN:30,TestGuard.disallowExecution
+FNDA:2,TestGuard.disallowExecution
+DA:31,2
+FN:34,TestGuard.checkTransaction
+FNDA:6,TestGuard.checkTransaction
+DA:47,6
+BRDA:47,0,0,-
+BRDA:47,0,1,6
+DA:48,6
+BRDA:48,1,0,2
+BRDA:48,1,1,4
+DA:49,4
+BRDA:49,2,0,-
+BRDA:49,2,1,4
+DA:50,4
+BRDA:50,3,0,-
+BRDA:50,3,1,4
+DA:51,4
+FN:54,TestGuard.checkAfterExecution
+FNDA:4,TestGuard.checkAfterExecution
+DA:56,4
+BRDA:56,4,0,2
+BRDA:56,4,1,2
+DA:58,2
+FN:61,TestGuard.setUp
+FNDA:600,TestGuard.setUp
+DA:62,600
+DA:63,600
+FNF:6
+FNH:5
+LF:13
+LH:12
+BRF:10
+BRH:7
+end_of_record
diff --git a/lib/ERC1155/ERC1155.sol b/lib/ERC1155/ERC1155.sol
deleted file mode 100644
index a590021..0000000
--- a/lib/ERC1155/ERC1155.sol
+++ /dev/null
@@ -1,261 +0,0 @@
-// SPDX-License-Identifier: AGPL-3.0-only
-pragma solidity >=0.8.0;
-
-/// @notice Minimalist and gas efficient standard ERC1155 implementation.
-/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC1155.sol)
-abstract contract ERC1155 {
- /*//////////////////////////////////////////////////////////////
- EVENTS
- //////////////////////////////////////////////////////////////*/
-
- event TransferSingle(
- address indexed operator,
- address indexed from,
- address indexed to,
- uint256 id,
- uint256 amount
- );
-
- event TransferBatch(
- address indexed operator,
- address indexed from,
- address indexed to,
- uint256[] ids,
- uint256[] amounts
- );
-
- event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
-
- event URI(string value, uint256 indexed id);
-
- /*//////////////////////////////////////////////////////////////
- ERC1155 STORAGE
- //////////////////////////////////////////////////////////////*/
-
- mapping(address => mapping(uint256 => uint256)) internal _balanceOf;
-
- mapping(address => mapping(address => bool)) public isApprovedForAll;
-
- /*//////////////////////////////////////////////////////////////
- METADATA LOGIC
- //////////////////////////////////////////////////////////////*/
-
- function uri(uint256 id) public view virtual returns (string memory);
-
- /*//////////////////////////////////////////////////////////////
- ERC1155 LOGIC
- //////////////////////////////////////////////////////////////*/
-
- function setApprovalForAll(address operator, bool approved) public virtual {
- isApprovedForAll[msg.sender][operator] = approved;
-
- emit ApprovalForAll(msg.sender, operator, approved);
- }
-
- function safeTransferFrom(
- address from,
- address to,
- uint256 id,
- uint256 amount,
- bytes calldata data
- ) public virtual {
- require(msg.sender == from || isApprovedForAll[from][msg.sender], "NOT_AUTHORIZED");
-
- _balanceOf[from][id] -= amount;
- _balanceOf[to][id] += amount;
-
- emit TransferSingle(msg.sender, from, to, id, amount);
-
- require(
- to.code.length == 0
- ? to != address(0)
- : ERC1155TokenReceiver(to).onERC1155Received(msg.sender, from, id, amount, data) ==
- ERC1155TokenReceiver.onERC1155Received.selector,
- "UNSAFE_RECIPIENT"
- );
- }
-
- function safeBatchTransferFrom(
- address from,
- address to,
- uint256[] calldata ids,
- uint256[] calldata amounts,
- bytes calldata data
- ) public virtual {
- require(ids.length == amounts.length, "LENGTH_MISMATCH");
-
- require(msg.sender == from || isApprovedForAll[from][msg.sender], "NOT_AUTHORIZED");
-
- // Storing these outside the loop saves ~15 gas per iteration.
- uint256 id;
- uint256 amount;
-
- for (uint256 i = 0; i < ids.length; ) {
- id = ids[i];
- amount = amounts[i];
-
- _balanceOf[from][id] -= amount;
- _balanceOf[to][id] += amount;
-
- // An array can't have a total length
- // larger than the max uint256 value.
- unchecked {
- ++i;
- }
- }
-
- emit TransferBatch(msg.sender, from, to, ids, amounts);
-
- require(
- to.code.length == 0
- ? to != address(0)
- : ERC1155TokenReceiver(to).onERC1155BatchReceived(msg.sender, from, ids, amounts, data) ==
- ERC1155TokenReceiver.onERC1155BatchReceived.selector,
- "UNSAFE_RECIPIENT"
- );
- }
-
- function balanceOf(address owner, uint256 id) public view virtual returns (uint256 balance) {
- balance = _balanceOf[owner][id];
- }
-
- function balanceOfBatch(address[] calldata owners, uint256[] calldata ids)
- public
- view
- virtual
- returns (uint256[] memory balances)
- {
- require(owners.length == ids.length, "LENGTH_MISMATCH");
-
- balances = new uint256[](owners.length);
-
- // Unchecked because the only math done is incrementing
- // the array index counter which cannot possibly overflow.
- unchecked {
- for (uint256 i = 0; i < owners.length; ++i) {
- balances[i] = _balanceOf[owners[i]][ids[i]];
- }
- }
- }
-
- /*//////////////////////////////////////////////////////////////
- ERC165 LOGIC
- //////////////////////////////////////////////////////////////*/
-
- function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
- return
- interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
- interfaceId == 0xd9b67a26 || // ERC165 Interface ID for ERC1155
- interfaceId == 0x0e89341c; // ERC165 Interface ID for ERC1155MetadataURI
- }
-
- /*//////////////////////////////////////////////////////////////
- INTERNAL MINT/BURN LOGIC
- //////////////////////////////////////////////////////////////*/
-
- function _mint(
- address to,
- uint256 id,
- uint256 amount,
- bytes memory data
- ) internal virtual {
- _balanceOf[to][id] += amount;
-
- emit TransferSingle(msg.sender, address(0), to, id, amount);
-
- require(
- to.code.length == 0
- ? to != address(0)
- : ERC1155TokenReceiver(to).onERC1155Received(msg.sender, address(0), id, amount, data) ==
- ERC1155TokenReceiver.onERC1155Received.selector,
- "UNSAFE_RECIPIENT"
- );
- }
-
- function _batchMint(
- address to,
- uint256[] memory ids,
- uint256[] memory amounts,
- bytes memory data
- ) internal virtual {
- uint256 idsLength = ids.length; // Saves MLOADs.
-
- require(idsLength == amounts.length, "LENGTH_MISMATCH");
-
- for (uint256 i = 0; i < idsLength; ) {
- _balanceOf[to][ids[i]] += amounts[i];
-
- // An array can't have a total length
- // larger than the max uint256 value.
- unchecked {
- ++i;
- }
- }
-
- emit TransferBatch(msg.sender, address(0), to, ids, amounts);
-
- require(
- to.code.length == 0
- ? to != address(0)
- : ERC1155TokenReceiver(to).onERC1155BatchReceived(msg.sender, address(0), ids, amounts, data) ==
- ERC1155TokenReceiver.onERC1155BatchReceived.selector,
- "UNSAFE_RECIPIENT"
- );
- }
-
- function _batchBurn(
- address from,
- uint256[] memory ids,
- uint256[] memory amounts
- ) internal virtual {
- uint256 idsLength = ids.length; // Saves MLOADs.
-
- require(idsLength == amounts.length, "LENGTH_MISMATCH");
-
- for (uint256 i = 0; i < idsLength; ) {
- _balanceOf[from][ids[i]] -= amounts[i];
-
- // An array can't have a total length
- // larger than the max uint256 value.
- unchecked {
- ++i;
- }
- }
-
- emit TransferBatch(msg.sender, from, address(0), ids, amounts);
- }
-
- function _burn(
- address from,
- uint256 id,
- uint256 amount
- ) internal virtual {
- _balanceOf[from][id] -= amount;
-
- emit TransferSingle(msg.sender, from, address(0), id, amount);
- }
-}
-
-/// @notice A generic interface for a contract which properly accepts ERC1155 tokens.
-/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC1155.sol)
-abstract contract ERC1155TokenReceiver {
- function onERC1155Received(
- address,
- address,
- uint256,
- uint256,
- bytes calldata
- ) external virtual returns (bytes4) {
- return ERC1155TokenReceiver.onERC1155Received.selector;
- }
-
- function onERC1155BatchReceived(
- address,
- address,
- uint256[] calldata,
- uint256[] calldata,
- bytes calldata
- ) external virtual returns (bytes4) {
- return ERC1155TokenReceiver.onERC1155BatchReceived.selector;
- }
-}
diff --git a/lib/forge-std b/lib/forge-std
index 662ae0d..8f24d6b 160000
--- a/lib/forge-std
+++ b/lib/forge-std
@@ -1 +1 @@
-Subproject commit 662ae0d6936654c5d1fb79fc15f521de28edb60e
+Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa
diff --git a/lib/hats-auth b/lib/hats-auth
deleted file mode 160000
index 9bb63ec..0000000
--- a/lib/hats-auth
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 9bb63ecfeebc0f554de351d30c6dcf4ec8e49048
diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts
index 5b027e5..dbb6104 160000
--- a/lib/openzeppelin-contracts
+++ b/lib/openzeppelin-contracts
@@ -1 +1 @@
-Subproject commit 5b027e517e6aee69f4b4b2f5e78274ac8ee53513
+Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3
diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable
index 25aabd2..723f8ca 160000
--- a/lib/openzeppelin-contracts-upgradeable
+++ b/lib/openzeppelin-contracts-upgradeable
@@ -1 +1 @@
-Subproject commit 25aabd286e002a1526c345c8db259d57bdf0ad28
+Subproject commit 723f8cab09cdae1aca9ec9cc1cfa040c2d4b06c1
diff --git a/lib/safe-contracts b/lib/safe-contracts
deleted file mode 160000
index c36bcab..0000000
--- a/lib/safe-contracts
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit c36bcab46578a442862d043e12a83fec41143dec
diff --git a/lib/safe-smart-account b/lib/safe-smart-account
new file mode 160000
index 0000000..bf943f8
--- /dev/null
+++ b/lib/safe-smart-account
@@ -0,0 +1 @@
+Subproject commit bf943f80fec5ac647159d26161446ac5d716a294
diff --git a/lib/solady b/lib/solady
new file mode 160000
index 0000000..7deab02
--- /dev/null
+++ b/lib/solady
@@ -0,0 +1 @@
+Subproject commit 7deab021af0426307ae79d091c4d1e26e9e89cf0
diff --git a/lib/zodiac b/lib/zodiac
index 9ba076a..18b7575 160000
--- a/lib/zodiac
+++ b/lib/zodiac
@@ -1 +1 @@
-Subproject commit 9ba076ad1ec005dda2e6be259446622d7d8c0df1
+Subproject commit 18b7575bb342424537883f7ebe0a94cd7f3ec4f6
diff --git a/script/Demo.s.sol b/script/Demo.s.sol
deleted file mode 100644
index 0551f5b..0000000
--- a/script/Demo.s.sol
+++ /dev/null
@@ -1,105 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-import "forge-std/Script.sol";
-import "forge-std/Test.sol";
-import "../src/HatsSignerGateFactory.sol";
-import "../src/HatsSignerGate.sol";
-import "hats-protocol/Interfaces/IHats.sol";
-import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";
-
-contract Demo is Script {
- IHats public hats = IHats(0x2923469A33bd2FA2Ab33c877DB81d35A9D8d60C6);
- HatsSignerGateFactory public hsgFactory = HatsSignerGateFactory(0x397DFF38c6911216fd6A806e2840De93AD10c623);
- HatsSignerGate public hsg;
-
- address public demoLeader; // assign this
-
- uint256[] admins;
- string[] details;
- uint32[] maxSupplies;
- address[] eligibilities;
- address[] toggles;
- string[] images;
- uint256[] ids;
- address[] tos;
- bool[] mutables;
-
- function run() external {
- uint256 privKey = vm.envUint("PRIVATE_KEY");
- address deployer = vm.rememberKey(privKey);
- vm.startBroadcast(deployer);
-
- // 1. create top hat
- uint256 tophat = hats.mintTopHat(deployer, "", "");
-
- // 2. find Core Unit hat id
- uint256 coreUnit = hats.getNextId(tophat);
- // 3. find Facilitator hat id
- uint256 facilitator = hats.getNextId(coreUnit);
- // 4. find Member hat id
- uint256 member = hats.getNextId(facilitator);
-
- // 5. deploy HatsSignerGate, with Core Unit hat as owner and Member hat as signer
- (address hsg_, address safe) = hsgFactory.deployHatsSignerGateAndSafe(
- coreUnit, // owner hat
- member, // signer hat
- 2, // min threshold
- 3, // target threshold
- 5 // max signers
- );
- hsg = HatsSignerGate(hsg_);
-
- // 6. create Core Unit hat, with multisig as eligibility and toggle
- // 7. create Facilitator hat, with multisig as eligibility and toggle
- // 8. create Member hat, with multisig as eligibility and toggle
- admins[0] = tophat;
- admins[1] = coreUnit;
- admins[2] = facilitator;
- details[0] = "Demo Core Unit";
- details[1] = "Demo Core Unit Facilitator";
- details[2] = "Demo Core Unit Member";
- maxSupplies[0] = 1;
- maxSupplies[1] = 1;
- maxSupplies[2] = 5;
- eligibilities[0] = safe;
- eligibilities[1] = safe;
- eligibilities[2] = safe;
- toggles[0] = safe;
- toggles[1] = safe;
- toggles[2] = safe;
- images[0] = "";
- images[1] = "";
- images[2] = "";
- mutables[0] = true;
- mutables[1] = true;
- mutables[2] = true;
-
- hats.batchCreateHats(
- admins, // admins
- details, // details
- maxSupplies, // max supplies
- eligibilities, // eligibility
- toggles, // toggles
- mutables,
- images // imageURIs
- );
-
- // 9. mint Core Unit hat to multisig
- // 10. mint Facilitator hat to demo lead
- ids[0] = coreUnit;
- ids[1] = facilitator;
- tos[0] = safe;
- tos[1] = demoLeader;
-
- hats.batchMintHats(ids, tos);
-
- vm.stopBroadcast();
- }
-
- // // simulation
- // forge script script/HatsSignerGateFactory.s.sol -f goerli
-
- // // actual deploy
- // forge script script/HatsSignerGateFactory.s.sol -f goerli --broadcast --verify
-}
diff --git a/script/DeployParams.json b/script/DeployParams.json
index ff4bf20..8e41275 100644
--- a/script/DeployParams.json
+++ b/script/DeployParams.json
@@ -1,74 +1,66 @@
{
"100": {
- "gnosisFallbackLibrary": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4",
- "gnosisMultisendLibrary": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761",
- "gnosisSafeProxyFactory": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2",
"hatsProtocol": "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137",
- "moduleProxyFactory": "0x00000000000DC7F163742Eb4aBEf650037b1f588",
- "safeSingleton": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552"
+ "safeFallbackLibrary": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99",
+ "safeMultisendLibrary": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2",
+ "safeProxyFactory": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67",
+ "safeSingleton": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762",
+ "zodiacModuleFactory": "0x000000000000aDdB49795b0f9bA5BC298cDda236"
},
"1": {
- "gnosisFallbackLibrary": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4",
- "gnosisMultisendLibrary": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761",
- "gnosisSafeProxyFactory": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2",
"hatsProtocol": "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137",
- "moduleProxyFactory": "0x00000000000DC7F163742Eb4aBEf650037b1f588",
- "safeSingleton": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552"
- },
- "5": {
- "gnosisFallbackLibrary": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4",
- "gnosisMultisendLibrary": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761",
- "gnosisSafeProxyFactory": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2",
- "hatsProtocol": "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137",
- "moduleProxyFactory": "0x00000000000DC7F163742Eb4aBEf650037b1f588",
- "safeSingleton": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552"
+ "safeFallbackLibrary": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99",
+ "safeMultisendLibrary": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2",
+ "safeProxyFactory": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67",
+ "safeSingleton": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762",
+ "zodiacModuleFactory": "0x000000000000aDdB49795b0f9bA5BC298cDda236"
},
"10": {
- "gnosisFallbackLibrary": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804",
- "gnosisMultisendLibrary": "0x998739BFdAAdde7C933B942a68053933098f9EDa",
- "gnosisSafeProxyFactory": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC",
"hatsProtocol": "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137",
- "moduleProxyFactory": "0x00000000000DC7F163742Eb4aBEf650037b1f588",
- "safeSingleton": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA"
+ "safeFallbackLibrary": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99",
+ "safeMultisendLibrary": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2",
+ "safeProxyFactory": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67",
+ "safeSingleton": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762",
+ "zodiacModuleFactory": "0x000000000000aDdB49795b0f9bA5BC298cDda236"
},
"137": {
- "gnosisFallbackLibrary": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4",
- "gnosisMultisendLibrary": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761",
- "gnosisSafeProxyFactory": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2",
"hatsProtocol": "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137",
- "moduleProxyFactory": "0x00000000000DC7F163742Eb4aBEf650037b1f588",
- "safeSingleton": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E"
+ "safeFallbackLibrary": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99",
+ "safeMultisendLibrary": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2",
+ "safeProxyFactory": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67",
+ "safeSingleton": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762",
+ "zodiacModuleFactory": "0x000000000000aDdB49795b0f9bA5BC298cDda236"
},
"42161": {
- "gnosisFallbackLibrary": "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4",
- "gnosisMultisendLibrary": "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761",
- "gnosisSafeProxyFactory": "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2",
"hatsProtocol": "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137",
- "moduleProxyFactory": "0x00000000000DC7F163742Eb4aBEf650037b1f588",
- "safeSingleton": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E"
+ "safeFallbackLibrary": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99",
+ "safeMultisendLibrary": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2",
+ "safeProxyFactory": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67",
+ "safeSingleton": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762",
+ "zodiacModuleFactory": "0x000000000000aDdB49795b0f9bA5BC298cDda236"
},
"11155111": {
- "gnosisFallbackLibrary": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804",
- "gnosisMultisendLibrary": "0x998739BFdAAdde7C933B942a68053933098f9EDa",
- "gnosisSafeProxyFactory": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC",
"hatsProtocol": "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137",
- "moduleProxyFactory": "0x000000000000aDdB49795b0f9bA5BC298cDda236",
- "safeSingleton": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938"
+ "safeFallbackLibrary": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99",
+ "safeMultisendLibrary": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2",
+ "safeProxyFactory": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67",
+ "safeSingleton": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762",
+ "zodiacModuleFactory": "0x000000000000aDdB49795b0f9bA5BC298cDda236"
},
"8453": {
- "gnosisFallbackLibrary": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804",
- "gnosisMultisendLibrary": "0x998739BFdAAdde7C933B942a68053933098f9EDa",
- "gnosisSafeProxyFactory": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC",
"hatsProtocol": "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137",
- "moduleProxyFactory": "0x000000000000aDdB49795b0f9bA5BC298cDda236",
- "safeSingleton": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA"
+ "safeFallbackLibrary": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99",
+ "safeMultisendLibrary": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2",
+ "safeProxyFactory": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67",
+ "safeSingleton": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762",
+ "zodiacModuleFactory": "0x000000000000aDdB49795b0f9bA5BC298cDda236"
},
"42220": {
- "gnosisFallbackLibrary": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804",
- "gnosisMultisendLibrary": "0x998739BFdAAdde7C933B942a68053933098f9EDa",
- "gnosisSafeProxyFactory": "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC",
"hatsProtocol": "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137",
- "moduleProxyFactory": "0x000000000000aDdB49795b0f9bA5BC298cDda236",
- "safeSingleton": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA"
+ "safeFallbackLibrary": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99",
+ "safeMultisendLibrary": "0x9641d764fc13c8B624c04430C7356C1C7C8102e2",
+ "safeProxyFactory": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67",
+ "safeSingleton": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762",
+ "zodiacModuleFactory": "0x000000000000aDdB49795b0f9bA5BC298cDda236"
}
-}
+}
\ No newline at end of file
diff --git a/script/HatsSignerGate.s.sol b/script/HatsSignerGate.s.sol
index 2b5e225..ba33ac5 100644
--- a/script/HatsSignerGate.s.sol
+++ b/script/HatsSignerGate.s.sol
@@ -1,38 +1,241 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
-import "forge-std/Script.sol";
-import "../src/HatsSignerGate.sol";
-import "../src/HatsSignerGateFactory.sol";
-
-contract DeployHatsSignerGate is Script {
- HatsSignerGateFactory public hsgFactory; // to deploy
- uint256 public ownerHatId = 80879840001451919384001045261058892020911433267621717443310830747648;
- uint256 public signersHatId = 80985152293120476570698963288742562453230328363022266554565141725184;
- address public safe = 0x56c7A84Cf42Cfe70BfdF14140747ffc63b96E51A;
- // address public hats = 0x245e5B56C18B18aC2d72F94C5F7bE1D52497A8aD;
- uint256 public minThreshold = 3;
- uint256 public targetThreshold = 3;
- uint256 public maxSigners = 9;
- // string public version = "MC Super Scouts Demo #1";
- // string public version = "Rinkeby test #5";
-
- string public version = "Cub Scouts Beta 02";
-
- function run() external {
- uint256 privKey = vm.envUint("PRIVATE_KEY");
- address deployer = vm.rememberKey(privKey);
- vm.startBroadcast(deployer);
-
- /* ddress hatsSignerGate = */
- hsgFactory.deployHatsSignerGate(ownerHatId, signersHatId, safe, minThreshold, targetThreshold, maxSigners);
-
- vm.stopBroadcast();
+import { Script, console2 } from "forge-std/Script.sol";
+import { stdJson } from "forge-std/StdJson.sol";
+import { HatsSignerGate } from "../src/HatsSignerGate.sol";
+import { IHatsSignerGate } from "../src/interfaces/IHatsSignerGate.sol";
+import { ModuleProxyFactory } from "../lib/zodiac/contracts/factory/ModuleProxyFactory.sol";
+
+contract BaseScript is Script {
+ bool public verbose = true;
+
+ function getChainKey() internal view returns (string memory) {
+ return string.concat(".", vm.toString(block.chainid));
+ }
+}
+
+contract DeployImplementation is BaseScript {
+ using stdJson for string;
+
+ address public hats;
+ address public safeFallbackLibrary;
+ address public safeMultisendLibrary;
+ address public safeProxyFactory;
+ address public safeSingleton;
+ address public zodiacModuleFactory;
+
+ HatsSignerGate public implementation;
+
+ /// ===========================================
+ /// @dev deployment params to be set manually
+ bytes32 public SALT = bytes32(abi.encode(0x4a75)); // ~ H(4) A(a) T(7) S(6)
+ /// ===========================================
+
+ function setDeployParams() public {
+ string memory root = vm.projectRoot();
+ string memory path = string.concat(root, "/script/DeployParams.json");
+ string memory json = vm.readFile(path);
+ string memory chain = getChainKey();
+
+ bytes memory params = json.parseRaw(chain);
+
+ // the json is parsed in alphabetical order, so we decode it that way too
+ (hats, safeFallbackLibrary, safeMultisendLibrary, safeProxyFactory, safeSingleton, zodiacModuleFactory) =
+ abi.decode(params, (address, address, address, address, address, address));
+ }
+
+ function prepare(bool _verbose) public {
+ verbose = _verbose;
+ }
+
+ function log(bool _verbose) public view {
+ if (_verbose) {
+ console2.log("HSG implementation", address(implementation));
+ console2.log("HSG runtime bytecode size:", address(implementation).code.length);
+
+ uint256 codeLength = address(implementation).code.length;
+ if (codeLength > 24_576) {
+ console2.log("HSG runtime bytecode margin: negative", codeLength - 24_576);
+ } else {
+ console2.log("HSG runtime bytecode margin: positive", 24_576 - codeLength);
+ }
+
+ console2.log("Safe singleton", safeSingleton);
+ console2.log("Safe fallback library", safeFallbackLibrary);
+ console2.log("Safe multisend library", safeMultisendLibrary);
+ console2.log("Safe proxy factory", safeProxyFactory);
}
+ }
+
+ function run() external virtual returns (HatsSignerGate) {
+ setDeployParams();
+ uint256 privKey = vm.envUint("PRIVATE_KEY");
+ address deployer = vm.rememberKey(privKey);
+ vm.startBroadcast(deployer);
+
+ implementation =
+ new HatsSignerGate{ salt: SALT }(hats, safeSingleton, safeFallbackLibrary, safeMultisendLibrary, safeProxyFactory);
+
+ vm.stopBroadcast();
+
+ log(verbose);
+
+ return implementation;
+ }
+
+ /*
+
+ forge script script/HatsSignerGate.s.sol:DeployImplementation --via-ir -f sepolia
+ forge script script/HatsSignerGate.s.sol:DeployImplementation --via-ir -f sepolia --broadcast --verify
+
+ forge verify-contract --chain-id --num-of-optimizations 1000000 --watch --constructor-args 0000000000000000000000003bc1a0ad72417f2d411118085256fc53cbddd13700000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c762000000000000000000000000fd0732dc9e303f09fcef3a7388ad10a83459ec990000000000000000000000009641d764fc13c8b624c04430c7356c1c7c8102e20000000000000000000000004e1dcf7ad4e460cfd30791ccc4f9c8a4f820ec67 --compiler-version v0.8.28 0x148057884AC910Bdd93693F230C5c35a8c47CA3b src/HatsSignerGate.sol:HatsSignerGate --etherscan-api-key $ETHERSCAN_KEY
+
+ */
+}
+
+contract MultiChainDeployImplementation is DeployImplementation {
+ using stdJson for string;
+
+ string[] public chains = ["arbitrum", "base", "celo", "gnosis", /*"mainnet",*/ "optimism", "polygon"/*, "sepolia"*/];
+
+ function run() external override returns (HatsSignerGate) {
+ uint256 privKey = vm.envUint("PRIVATE_KEY");
+ address deployer = vm.rememberKey(privKey);
+
+ for (uint256 i = 0; i < chains.length; i++) {
+ string memory chain = chains[i];
+ console2.log("\nDeploying to", chain);
+
+ // Use forge's built-in --fork-url flag to switch networks
+ vm.createSelectFork(vm.rpcUrl(chain));
+
+ // set the params for the current chain
+ setDeployParams();
+
+ // deploy the implementation with forge's built-in CREATE2 factory
+ vm.startBroadcast(deployer);
+ try new HatsSignerGate{ salt: SALT }(
+ hats, safeSingleton, safeFallbackLibrary, safeMultisendLibrary, safeProxyFactory
+ ) returns (HatsSignerGate hsg) {
+ implementation = hsg;
+ log(verbose);
+ } catch {
+ console2.log("Deployment failed on", chain);
+ }
+ vm.stopBroadcast();
+ }
+
+ return implementation;
+ }
+
+ /*
+
+ forge script script/HatsSignerGate.s.sol:MultiChainDeployImplementation --via-ir
+ forge script script/HatsSignerGate.s.sol:MultiChainDeployImplementation --via-ir --broadcast --verify
+
+ */
+}
+
+contract DeployInstance is BaseScript {
+ using stdJson for string;
+
+ address public zodiacModuleFactory;
+ address public hats;
+ address public implementation = 0x148057884AC910Bdd93693F230C5c35a8c47CA3b;
+ address public instance;
+ address public hsgGuard;
+ address[] public hsgModules;
+ uint256 public saltNonce = 1;
+
+ uint256 public ownerHat = 0x000002ae00000000000000000000000000000000000000000000000000000000;
+ uint256[] public signersHats = [0x000002ae00010002000000000000000000000000000000000000000000000000];
+ IHatsSignerGate.ThresholdConfig public thresholdConfig =
+ IHatsSignerGate.ThresholdConfig({ thresholdType: IHatsSignerGate.TargetThresholdType.ABSOLUTE, min: 1, target: 2 });
+ address public safe = address(0);
+ bool public locked = false;
+ bool public claimableFor = true;
+
+ function prepare1(
+ address _implementation,
+ uint256 _ownerHat,
+ uint256[] memory _signersHats,
+ IHatsSignerGate.ThresholdConfig memory _thresholdConfig,
+ address _safe,
+ bool _locked,
+ bool _claimableFor,
+ address _hsgGuard,
+ address[] memory _hsgModules
+ ) public {
+ implementation = _implementation;
+ ownerHat = _ownerHat;
+ signersHats = _signersHats;
+ thresholdConfig = _thresholdConfig;
+ safe = _safe;
+ locked = _locked;
+ claimableFor = _claimableFor;
+ hsgGuard = _hsgGuard;
+ hsgModules = _hsgModules;
+ }
+
+ function prepare2(bool _verbose, uint256 _saltNonce) public {
+ verbose = _verbose;
+ saltNonce = _saltNonce;
+ }
+
+ function setModuleFactory() public {
+ string memory root = vm.projectRoot();
+ string memory path = string.concat(root, "/script/DeployParams.json");
+ string memory json = vm.readFile(path);
+ string memory chain = getChainKey();
+
+ bytes memory params = json.parseRaw(chain);
+
+ // the json is parsed in alphabetical order, so we decode it that way too
+ (,,,,, zodiacModuleFactory) = abi.decode(params, (address, address, address, address, address, address));
+ }
+
+ function setupParams() public view returns (IHatsSignerGate.SetupParams memory params) {
+ params = IHatsSignerGate.SetupParams({
+ ownerHat: ownerHat,
+ signerHats: signersHats,
+ safe: safe,
+ thresholdConfig: thresholdConfig,
+ locked: locked,
+ claimableFor: claimableFor,
+ implementation: implementation,
+ hsgGuard: hsgGuard,
+ hsgModules: hsgModules
+ });
+ return params;
+ }
+
+ function run() external returns (HatsSignerGate) {
+ setModuleFactory();
+
+ uint256 privKey = vm.envUint("PRIVATE_KEY");
+ address deployer = vm.rememberKey(privKey);
+ vm.startBroadcast(deployer);
+
+ instance = ModuleProxyFactory(zodiacModuleFactory).deployModule(
+ address(implementation), abi.encodeWithSignature("setUp(bytes)", abi.encode(setupParams())), saltNonce
+ );
+
+ vm.stopBroadcast();
+
+ if (verbose) {
+ if (safe == address(0)) {
+ console2.log("new Safe deployed", address(HatsSignerGate(instance).safe()));
+ }
+ }
+
+ return HatsSignerGate(instance);
+ }
- // forge script script/HatsSignerGate.s.sol:DeployHatsSignerGate --rpc-url $RINKEBY_RPC --verify --etherscan-api-key $ETHERSCAN_KEY --broadcast
+ /*
- // forge script script/HatsSignerGate.s.sol:DeployHatsSignerGate --rpc-url $GC_RPC --private-key $PRIVATE_KEY --verify --etherscan-api-key $GNOSISSCAN_KEY --broadcast
+ forge script script/HatsSignerGate.s.sol:DeployInstance --via-ir -f sepolia
+ forge script script/HatsSignerGate.s.sol:DeployInstance --via-ir -f sepolia --broadcast
- // forge script script/HatsSignerGate.s.sol:DeployHatsSignerGate --rpc-url $GC_RPC --verify --etherscan-api-key $GNOSISSCAN_KEY --broadcast
+ */
}
diff --git a/script/HatsSignerGateFactory.s.sol b/script/HatsSignerGateFactory.s.sol
deleted file mode 100644
index 89441a9..0000000
--- a/script/HatsSignerGateFactory.s.sol
+++ /dev/null
@@ -1,100 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-import "forge-std/Script.sol";
-import "forge-std/StdJson.sol";
-import "forge-std/Test.sol";
-import "../src/HatsSignerGateFactory.sol";
-import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";
-
-contract DeployHatsSignerGateFactory is Script {
- using stdJson for string;
- // deployment params to be read from DeployParams.json
-
- address public gnosisFallbackLibrary;
- address public gnosisMultisendLibrary;
- address public gnosisSafeProxyFactory;
- address public hats;
- address public moduleProxyFactory;
- address public safeSingleton;
-
- HatsSignerGateFactory public factory;
-
- /// ===========================================
- /// @dev deployment params to be set manually
- string public version = "1.2-beta";
- bytes32 public SALT = bytes32(abi.encode(0x4a75)); // ~ H(4) A(a) T(7) S(5)
-
- /// @dev set to true to deploy singletons, false to use existing deployments below
- bool public deploySingletones = true;
- HatsSignerGate public hsgSingleton = HatsSignerGate(0x844b3c7781338D3308Eb8D64727033893fcE1432);
- MultiHatsSignerGate public mhsgSingleton = MultiHatsSignerGate(0xca9d698Adb4052Ac7751019D69582950B1E42b43);
- /// ===========================================
-
- function getChainKey() public view returns (string memory) {
- return string.concat(".", vm.toString(block.chainid));
- }
-
- function setDeployParams() public {
- string memory root = vm.projectRoot();
- string memory path = string.concat(root, "/script/DeployParams.json");
- string memory json = vm.readFile(path);
- string memory chain = getChainKey();
-
- bytes memory params = json.parseRaw(chain);
-
- // the json is parsed in alphabetical order, so we decode it that way too
- (gnosisFallbackLibrary, gnosisMultisendLibrary, gnosisSafeProxyFactory, hats, moduleProxyFactory, safeSingleton)
- = abi.decode(params, (address, address, address, address, address, address));
- }
-
- function run() external {
- setDeployParams();
- uint256 privKey = vm.envUint("PRIVATE_KEY");
- address deployer = vm.rememberKey(privKey);
- // console2.log("deployer", deployer);
- console2.log("deployer balance (wei):", deployer.balance);
- vm.startBroadcast(deployer);
-
- if (deploySingletones) {
- // deploy singletons
- hsgSingleton = new HatsSignerGate{ salt: SALT }();
- mhsgSingleton = new MultiHatsSignerGate{ salt: SALT }();
- }
-
- // deploy factory
- factory = new HatsSignerGateFactory{ salt: SALT }(
- address(hsgSingleton),
- address(mhsgSingleton),
- hats,
- safeSingleton,
- gnosisFallbackLibrary,
- gnosisMultisendLibrary,
- gnosisSafeProxyFactory,
- moduleProxyFactory,
- version
- );
-
- vm.stopBroadcast();
-
- console.log("factory address", address(factory));
- console.log("hsg address", address(hsgSingleton));
- console.log("mhsg address", address(mhsgSingleton));
-
- // uncomment to check if its working correctly when simulating
- // (address hsg, address safe) = factory.deployHatsSignerGateAndSafe(1, 2, 3, 4, 5);
- // GnosisSafe _safe = GnosisSafe(payable(safe));
- // console2.log("safe threshold", _safe.getThreshold());
- // console2.log("hsg is module", _safe.isModuleEnabled(hsg));
- }
-
- // // simulation
- // forge script script/HatsSignerGateFactory.s.sol:DeployHatsSignerGateFactory -f gnosis
-
- // // actual deploy
- // forge script script/HatsSignerGateFactory.s.sol -f goerli --broadcast --verify
-
- // forge verify-contract --chain-id 5 --num-of-optimizations 1000000 --watch --constructor-args $(cast abi-encode "constructor(address,address,address,address,address,address,address,address,string)" 0x844b3c7781338D3308Eb8D64727033893fcE1432 0xca9d698adb4052ac7751019d69582950b1e42b43 0x9D2dfd6066d5935267291718E8AA16C8Ab729E9d 0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552 0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4 0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761 0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2 0x00000000000DC7F163742Eb4aBEf650037b1f588 "1.0-beta") --compiler-version v0.8.17 0x5Ba1E49a2efCd5589422FdF1F6BCE37e4A288611 src/HatsSignerGateFactory.sol:HatsSignerGateFactory --etherscan-api-key $ETHERSCAN_KEY
-
- // forge verify-contract --chain-id 5 --num-of-optimizations 1000000 --watch --compiler-version v0.8.17 0xca9d698adb4052ac7751019d69582950b1e42b43 src/MultiHatsSignerGate.sol:MultiHatsSignerGate --etherscan-api-key $ETHERSCAN_KEY
-}
diff --git a/src/HSGLib.sol b/src/HSGLib.sol
deleted file mode 100644
index 9b75168..0000000
--- a/src/HSGLib.sol
+++ /dev/null
@@ -1,82 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity >=0.8.13;
-
-library HSGLib {
- /// @notice Emitted when a new target signature threshold for the `safe` is set
- event TargetThresholdSet(uint256 threshold);
-
- /// @notice Emitted when a new minimum signature threshold for the `safe` is set
- event MinThresholdSet(uint256 threshold);
-
- /// @notice Emitted when new approved signer hats are added
- event SignerHatsAdded(uint256[] newSignerHats);
-}
-
-/// @notice Signers are not allowed to disable the HatsSignerGate guard
-error CannotDisableThisGuard(address guard);
-
-/// @notice Only the wearer of the owner Hat can make changes to this contract
-error NotOwnerHatWearer(address user);
-
-/// @notice Only wearers of a valid signer hat can become signers
-error NotSignerHatWearer(address user);
-
-/// @notice Valid signers must wear the signer hat at time of execution
-error InvalidSigners();
-
-/// @notice This contract can only be set once as a zodiac guard on `safe`
-error GuardAlreadySet();
-
-/// @notice Can't remove a signer if they're still wearing the signer hat
-error StillWearsSignerHat(address signer);
-
-/// @notice Can never have more signers than designated by `maxSigners`
-error MaxSignersReached();
-
-/// @notice Emitted when a valid signer attempts `claimSigner` but there are already `maxSigners` signers
-/// @dev This will only occur if `signerCount` is out of sync with the current number of valid signers, which can be resolved by calling `reconcileSignerCount`
-error NoInvalidSignersToReplace();
-
-/// @notice Target threshold must be lower than `maxSigners`
-error InvalidTargetThreshold();
-
-/// @notice Min threshold cannot be higher than `maxSigners` or `targetThreshold`
-error InvalidMinThreshold();
-
-/// @notice Signers already on the `safe` cannot claim twice
-error SignerAlreadyClaimed(address signer);
-
-/// @notice Emitted when a call to change the threshold fails
-error FailedExecChangeThreshold();
-
-/// @notice Emitted when a call to add a signer fails
-error FailedExecAddSigner();
-
-/// @notice Emitted when a call to remove a signer fails
-error FailedExecRemoveSigner();
-
-/// @notice Emitted when a call to enable a module fails
-error FailedExecEnableModule();
-
-/// @notice Cannot exececute a tx if `safeOnwerCount` < `minThreshold`
-error BelowMinThreshold(uint256 minThreshold, uint256 safeOwnerCount);
-
-/// @notice Can only claim signer with a valid signer hat
-error InvalidSignerHat(uint256 hatId);
-
-/// @notice Signers are not allowed to change the threshold
-error SignersCannotChangeThreshold();
-
-/// @notice Signers are not allowed to add new modules
-error SignersCannotChangeModules();
-
-/// @notice Signers are not allowed to change owners
-error SignersCannotChangeOwners();
-
-/// @notice Emmitted when a call to `checkTransaction` or `checkAfterExecution` is not made from the `safe`
-/// @dev Together with `guardEntries`, protects against arbitrary reentrancy attacks by the signers
-error NotCalledFromSafe();
-
-/// @notice Emmitted when attempting to reenter `checkTransaction`
-/// @dev The Safe will catch this error and re-throw with its own error message (`GS013`)
-error NoReentryAllowed();
diff --git a/src/HatsSignerGate.sol b/src/HatsSignerGate.sol
index 56667e0..fc7b4ed 100644
--- a/src/HatsSignerGate.sol
+++ b/src/HatsSignerGate.sol
@@ -1,76 +1,1021 @@
-// SPDX-License-Identifier: MIT
-pragma solidity >=0.8.13;
-
-// import { Test, console2 } from "forge-std/Test.sol"; // remove after testing
-import { HatsSignerGateBase, IGnosisSafe, Enum } from "./HatsSignerGateBase.sol";
-import "./HSGLib.sol";
-
-contract HatsSignerGate is HatsSignerGateBase {
- uint256 public signersHatId;
-
- /// @notice Initializes a new instance of HatsSignerGate
- /// @dev Can only be called once
- /// @param initializeParams ABI-encoded bytes with initialization parameters
- function setUp(bytes calldata initializeParams) public payable override initializer {
- (
- uint256 _ownerHatId,
- uint256 _signersHatId,
- address _safe,
- address _hats,
- uint256 _minThreshold,
- uint256 _targetThreshold,
- uint256 _maxSigners,
- string memory _version,
- ) = abi.decode(
- initializeParams, (uint256, uint256, address, address, uint256, uint256, uint256, string, uint256)
- );
-
- _setUp(_ownerHatId, _safe, _hats, _minThreshold, _targetThreshold, _maxSigners, _version);
-
- signersHatId = _signersHatId;
- }
-
- /// @notice Function to become an owner on the safe if you are wearing the signers hat
- /// @dev Reverts if `maxSigners` has been reached, the caller is either invalid or has already claimed. Swaps caller with existing invalid owner if relevant.
- function claimSigner() public virtual {
- uint256 maxSigs = maxSigners; // save SLOADs
- address[] memory owners = safe.getOwners();
- uint256 currentSignerCount = _countValidSigners(owners);
-
- if (currentSignerCount >= maxSigs) {
- revert MaxSignersReached();
- }
+// SPDX-License-Identifier: LGPL-3.0
+pragma solidity >=0.8.28;
+
+// import { Test, console2 } from "../lib/forge-std/src/Test.sol"; // comment out after testing
+import { IHats } from "../lib/hats-protocol/src/Interfaces/IHats.sol";
+import { SafeManagerLib } from "./lib/SafeManagerLib.sol";
+import { IHatsSignerGate } from "./interfaces/IHatsSignerGate.sol";
+import { Initializable } from "../lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
+import { BaseGuard } from "../lib/zodiac/contracts/guard/BaseGuard.sol";
+import { GuardableUnowned } from "./lib/zodiac-modified/GuardableUnowned.sol";
+import { ModifierUnowned } from "./lib/zodiac-modified/ModifierUnowned.sol";
+import { Multicallable } from "../lib/solady/src/utils/Multicallable.sol";
+import { SignatureDecoder } from "../lib/safe-smart-account/contracts/common/SignatureDecoder.sol";
+import { ISafe, Enum } from "./lib/safe-interfaces/ISafe.sol";
+
+/// @title HatsSignerGate
+/// @author Haberdasher Labs
+/// @author @spengrah
+/// @author @gershido
+/// @notice A Zodiac compatible contract for managing a Safe's signers and signatures via Hats Protocol.
+/// - As a module on the Safe, it allows for signers to be added and removed based on Hats Protocol hats.
+/// - As a guard on the Safe, it ensures that transactions can only be executed by valid hat-wearing signers.
+/// - It also serves as a Zodiac modifier, allowing the Safe's functionality to be safely extended by attaching modules
+/// and a guard to HatsSignerGate itself.
+/// - An owner can control the HatsSignerGate's settings and behavior through various owner-only functions.
+/// @dev This contract is designed to work with the Zodiac Module Factory, from which instances are deployed.
+contract HatsSignerGate is
+ IHatsSignerGate,
+ BaseGuard,
+ GuardableUnowned,
+ ModifierUnowned,
+ Multicallable,
+ SignatureDecoder,
+ Initializable
+{
+ using SafeManagerLib for ISafe;
+
+ /*//////////////////////////////////////////////////////////////
+ CONSTANTS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @inheritdoc IHatsSignerGate
+ IHats public immutable HATS;
+
+ /// @dev The address of the Safe singleton contract used to deploy new Safes
+ address internal immutable SAFE_SINGLETON;
+
+ /// @dev The address of the Safe fallback library used to deploy new Safes
+ address internal immutable SAFE_FALLBACK_LIBRARY;
+
+ /// @dev The address of the Safe multisend library used to deploy new Safes
+ address internal immutable SAFE_MULTISEND_LIBRARY;
+
+ /// @dev The address of the Safe proxy factory used to deploy new Safes
+ address internal immutable SAFE_PROXY_FACTORY;
+
+ /// @inheritdoc IHatsSignerGate
+ string public constant version = "2.0.0";
+
+ /*//////////////////////////////////////////////////////////////
+ PUBLIC MUTABLE STATE
+ //////////////////////////////////////////////////////////////*/
+
+ /// @inheritdoc IHatsSignerGate
+ ISafe public safe;
+
+ /// @inheritdoc IHatsSignerGate
+ bool public locked;
+
+ /// @inheritdoc IHatsSignerGate
+ bool public claimableFor;
+
+ /// @inheritdoc IHatsSignerGate
+ address public implementation;
+
+ /// @inheritdoc IHatsSignerGate
+ uint256 public ownerHat;
+
+ /// @inheritdoc IHatsSignerGate
+ mapping(address => bool) public enabledDelegatecallTargets;
+
+ /// @inheritdoc IHatsSignerGate
+ mapping(address => uint256) public registeredSignerHats;
+
+ /*//////////////////////////////////////////////////////////////
+ INTERNAL MUTABLE STATE
+ //////////////////////////////////////////////////////////////*/
+
+ /// @dev Append-only tracker of approved signer hats
+ mapping(uint256 => bool) internal _validSignerHats;
+
+ /// @dev The threshold configuration
+ ThresholdConfig internal _thresholdConfig;
+
+ /*//////////////////////////////////////////////////////////////
+ TRANSIENT STATE
+ //////////////////////////////////////////////////////////////*/
+
+ /// @dev Temporary record of the existing owners on the `safe` when a transaction is submitted
+ bytes32 transient _existingOwnersHash;
+
+ /// @dev Temporary record of the existing threshold on the `safe` when a transaction is submitted
+ uint256 transient _existingThreshold;
+
+ /// @dev Temporary record of the existing fallback handler on the `safe` when a transaction is submitted
+ address transient _existingFallbackHandler;
+
+ /// @dev Temporary record of the operation type when a transaction is submitted
+ Enum.Operation transient _operation;
+
+ /// @dev Temporary record of whether we're inside an execTransaction call
+ bool transient _inSafeExecTransaction;
+
+ /// @dev Temporary record of whether we're inside an execTransactionFromModule call
+ bool transient _inModuleExecTransaction;
+
+ /// @dev The safe's nonce at the beginning of a transaction
+ uint256 transient _initialNonce;
+
+ /// @dev The number of times the checkTransaction function has been called in a transaction
+ uint256 transient _checkTransactionCounter;
+
+ /*//////////////////////////////////////////////////////////////
+ AUTHENTICATION FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @dev Internal function to check if the caller is wearing the owner hat
+ function _checkOwner() internal view {
+ if (!HATS.isWearerOfHat(msg.sender, ownerHat)) revert NotOwnerHatWearer();
+ }
+
+ /// @dev Internal function to check if the contract is unlocked
+ function _checkUnlocked() internal view {
+ if (locked) revert Locked();
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ CONSTRUCTOR
+ //////////////////////////////////////////////////////////////*/
+
+ constructor(
+ address _hats,
+ address _safeSingleton,
+ address _safeFallbackLibrary,
+ address _safeMultisendLibrary,
+ address _safeProxyFactory
+ ) initializer {
+ HATS = IHats(_hats);
+ SAFE_PROXY_FACTORY = _safeProxyFactory;
+ SAFE_SINGLETON = _safeSingleton;
+ SAFE_FALLBACK_LIBRARY = _safeFallbackLibrary;
+ SAFE_MULTISEND_LIBRARY = _safeMultisendLibrary;
+
+ // set the implementation's owner hat to a nonexistent hat to prevent state changes to the implementation
+ ownerHat = 1;
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ INITIALIZER
+ //////////////////////////////////////////////////////////////*/
+
+ /// @inheritdoc IHatsSignerGate
+ function setUp(bytes calldata initializeParams) public payable initializer {
+ SetupParams memory params = abi.decode(initializeParams, (SetupParams));
+
+ // deploy a new safe if there is no provided safe
+ if (params.safe == address(0)) {
+ params.safe = SafeManagerLib.deploySafeAndAttachHSG(
+ SAFE_PROXY_FACTORY, SAFE_SINGLETON, SAFE_FALLBACK_LIBRARY, SAFE_MULTISEND_LIBRARY
+ );
+ }
+
+ // set the instance's owner hat
+ _setOwnerHat(params.ownerHat);
+ // lock the instance if configured as such
+ if (params.locked) _lock();
+
+ // set the instance's claimableFor flag
+ _setClaimableFor(params.claimableFor);
+
+ // set the instance's safe and signer parameters
+ safe = ISafe(params.safe);
+ _addSignerHats(params.signerHats);
+ _setThresholdConfig(params.thresholdConfig);
+
+ // set the instance's metadata
+ implementation = params.implementation;
+
+ // initialize the modules linked list, and set initial modules, if any
+ setupModules();
+ for (uint256 i; i < params.hsgModules.length; ++i) {
+ _enableModule(params.hsgModules[i]);
+ }
+
+ // set the initial guard, if any
+ if (params.hsgGuard != address(0)) _setGuard(params.hsgGuard);
- if (safe.isOwner(msg.sender)) {
- revert SignerAlreadyClaimed(msg.sender);
+ // enable default delegatecall targets
+ _setDelegatecallTarget(0x40A2aCCbd92BCA938b02010E17A5b8929b49130D, true); // multisend-call-only v1.3.0 "canonical"
+ _setDelegatecallTarget(0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B, true); // multisend-call-only v1.3.0 "eip155"
+ _setDelegatecallTarget(0x9641d764fc13c8B624c04430C7356C1C7C8102e2, true); // multisend-call-only v1.4.1 "canonical"
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ PUBLIC FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @inheritdoc IHatsSignerGate
+ function claimSigner(uint256 _hatId) public {
+ // register the signer
+ _registerSigner({ _hatToRegister: _hatId, _signer: msg.sender, _allowReregistration: true });
+
+ // add the signer
+ _addSigner(msg.sender);
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function claimSignerFor(uint256 _hatId, address _signer) public {
+ // check that signer permissions are claimable for
+ if (!claimableFor) revert NotClaimableFor();
+
+ // register the signer, reverting if invalid or already registered
+ _registerSigner({ _hatToRegister: _hatId, _signer: _signer, _allowReregistration: false });
+
+ // add the signer
+ _addSigner(_signer);
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function claimSignersFor(uint256[] calldata _hatIds, address[] calldata _signers) public {
+ // check that signer permissions are claimable for
+ if (!claimableFor) revert NotClaimableFor();
+
+ // check that the arrays are the same length
+ uint256 toClaimCount = _signers.length;
+ if (_hatIds.length != toClaimCount) revert InvalidArrayLength();
+
+ ISafe s = safe;
+ // get the current threshold
+ uint256 threshold = s.getThreshold();
+ // get the current owners
+ address[] memory owners = s.getOwners();
+
+ // check if the only owner is this contract, meaning no owners have been added yet
+ bool isInitialOwnersState = owners.length == 1 && owners[0] == address(this);
+
+ // count the number of owners after the claim
+ uint256 newNumOwners = owners.length;
+
+ // iterate through the arrays, adding each signer
+ for (uint256 i; i < toClaimCount; ++i) {
+ uint256 hatId = _hatIds[i];
+ address signer = _signers[i];
+
+ // register the signer, reverting if invalid or already registered
+ _registerSigner({ _hatToRegister: hatId, _signer: signer, _allowReregistration: false });
+
+ // if the signer is not an owner, add them
+ if (!s.isOwner(signer)) {
+ // initiate the addOwnerData, to be conditionally set below
+ bytes memory addOwnerData;
+
+ // for the first signer, check if the only owner is this contract and swap it out if so
+ if (i == 0 && isInitialOwnersState) {
+ addOwnerData = SafeManagerLib.encodeSwapOwnerAction(SafeManagerLib.SENTINELS, address(this), signer);
+ } else {
+ // otherwise, add the claimer as a new owner
+ addOwnerData = SafeManagerLib.encodeAddOwnerWithThresholdAction(signer, threshold);
+ newNumOwners++;
}
- if (!isValidSigner(msg.sender)) {
- revert NotSignerHatWearer(msg.sender);
+ // execute the call
+ s.execSafeTransactionFromHSG(addOwnerData);
+ }
+ }
+
+ // update the threshold if necessary
+ uint256 newThreshold = _getNewThreshold(newNumOwners);
+ if (newThreshold != threshold) {
+ safe.execChangeThreshold(newThreshold);
+ }
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function removeSigner(address _signer) public {
+ if (isValidSigner(_signer)) revert StillWearsSignerHat();
+
+ // remove the signer from the safe and unregister them
+ _removeSigner(_signer);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ OWNER FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @inheritdoc IHatsSignerGate
+ function lock() public {
+ _checkUnlocked();
+ _checkOwner();
+ _lock();
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function setOwnerHat(uint256 _ownerHat) public {
+ _checkUnlocked();
+ _checkOwner();
+ _setOwnerHat(_ownerHat);
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function addSignerHats(uint256[] calldata _newSignerHats) external {
+ _checkUnlocked();
+ _checkOwner();
+ _addSignerHats(_newSignerHats);
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function setThresholdConfig(ThresholdConfig calldata _config) public {
+ _checkUnlocked();
+ _checkOwner();
+ _setThresholdConfig(_config);
+
+ // update the safe's threshold to match the new config
+ address[] memory owners = safe.getOwners();
+ // get the required amount of valid signatures according to the new threshold config
+ // and the current number of owners
+ uint256 newThreshold = _getRequiredValidSignatures(owners.length);
+ // the safe's threshold cannot be higher than the number of owners (safe's invariant)
+ if (newThreshold > owners.length) {
+ newThreshold = owners.length;
+ }
+
+ safe.execChangeThreshold(newThreshold);
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function setClaimableFor(bool _claimableFor) public {
+ _checkUnlocked();
+ _checkOwner();
+ _setClaimableFor(_claimableFor);
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function detachHSG() public {
+ _checkUnlocked();
+ _checkOwner();
+ ISafe s = safe; // save SLOAD
+
+ // first remove as guard, then as module
+ s.execRemoveHSGAsGuard();
+ s.execDisableHSGAsOnlyModule();
+ emit Detached();
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function migrateToNewHSG(address _newHSG, uint256[] calldata _signerHatIds, address[] calldata _signersToMigrate)
+ public
+ {
+ _checkUnlocked();
+ _checkOwner();
+
+ ISafe s = safe; // save SLOADS
+ // remove existing HSG as guard
+ s.execRemoveHSGAsGuard();
+ // enable new HSG as module and guard
+ s.execAttachNewHSG(_newHSG);
+ // remove existing HSG as module
+ s.execDisableHSGAsModule(_newHSG);
+
+ // if _signersToMigrate is provided, migrate them to the new HSG by calling claimSignersFor()
+ uint256 toMigrateCount = _signersToMigrate.length;
+ if (toMigrateCount > 0) {
+ // check that the arrays are the same length
+ if (_signerHatIds.length != toMigrateCount) revert InvalidArrayLength();
+
+ IHatsSignerGate(_newHSG).claimSignersFor(_signerHatIds, _signersToMigrate);
+ }
+ emit Migrated(_newHSG);
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function enableDelegatecallTarget(address _target) public {
+ _checkUnlocked();
+ _checkOwner();
+
+ _setDelegatecallTarget(_target, true);
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function disableDelegatecallTarget(address _target) public {
+ _checkUnlocked();
+ _checkOwner();
+
+ _setDelegatecallTarget(_target, false);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ ZODIAC GUARD FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @inheritdoc BaseGuard
+ /// @notice Only approved delegatecall targets are allowed.
+ /// @notice This function cannot be entered from within itself, called directly from within Safe.execTransaction, or
+ /// from within an execTransactionFromModule or execTransactionFromModuleReturnData call.
+ function checkTransaction(
+ address to,
+ uint256 value,
+ bytes memory data,
+ Enum.Operation operation,
+ uint256 safeTxGas,
+ uint256 baseGas,
+ uint256 gasPrice,
+ address gasToken,
+ address payable refundReceiver,
+ bytes memory signatures,
+ address // msgSender
+ ) public override {
+ // ensure that the call is coming from the safe
+ if (msg.sender != address(safe)) revert NotCalledFromSafe();
+
+ // Disallow entering this function from a) inside a Safe.execTransaction call or b) inside an
+ // execTransactionFromModule call
+ if (_inSafeExecTransaction || _inModuleExecTransaction) revert NoReentryAllowed();
+ _inSafeExecTransaction = true;
+
+ /// @dev We enforce that this function is called exactly once per Safe.execTransaction call. Leveraging the fact
+ /// that the Safe nonce increments exactly once per Safe.execTransaction call, we require that nonce diff matches
+ /// the number of times this function has been called.
+ // Record the initial nonce of the Safe at the beginning of the transaction
+ if (_checkTransactionCounter == 0) _initialNonce = safe.nonce() - 1;
+ // Increment the entrancy counter
+ _checkTransactionCounter++;
+ // Ensure that this function is called exactly once per Safe.execTransaction call
+ if (safe.nonce() - _initialNonce != _checkTransactionCounter) revert NoReentryAllowed();
+
+ // module guard preflight check
+ if (guard != address(0)) {
+ BaseGuard(guard).checkTransaction(
+ to,
+ value,
+ data,
+ operation,
+ // Zero out the redundant transaction information only used for Safe multisig transctions.
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(0),
+ "",
+ address(safe)
+ );
+ }
+
+ // get the existing owners and threshold
+ address[] memory owners = safe.getOwners();
+ uint256 threshold = safe.getThreshold();
+
+ // We record the operation type to guide the post-flight checks
+ _operation = operation;
+
+ if (operation == Enum.Operation.DelegateCall) {
+ // case: DELEGATECALL
+ // We disallow delegatecalls to unapproved targets
+ if (!enabledDelegatecallTargets[to]) revert DelegatecallTargetNotEnabled();
+
+ // Otherwise record the existing owners and threshold for post-flight checks to ensure that Safe state has not
+ // been altered
+ _existingOwnersHash = keccak256(abi.encode(owners));
+ _existingThreshold = threshold;
+ _existingFallbackHandler = safe.getSafeFallbackHandler();
+ } else if (to == address(safe)) {
+ // case: CALL to the safe
+ // We disallow external calls to the safe itself. Together with the above check, this ensures there are no
+ // unauthorized calls into the Safe itself
+ revert CannotCallSafe();
+ }
+
+ // case: CALL to a non-Safe target
+ // We can proceed to signer validation
+
+ // the safe's threshold is always the minimum between the required amount of valid signatures and the number of
+ // owners. if the threshold is lower than the required amount of valid signatures, it means that there are currently
+ // not enough owners to approve the tx, so we can revert without further checks
+ if (threshold != _getRequiredValidSignatures(owners.length)) revert ThresholdTooLow();
+
+ // get the tx hash
+ bytes32 txHash = safe.getTransactionHash(
+ to,
+ value,
+ data,
+ operation,
+ safeTxGas,
+ baseGas,
+ gasPrice,
+ gasToken,
+ refundReceiver,
+ // We subtract 1 since nonce was just incremented in the parent function call
+ safe.nonce() - 1
+ );
+
+ // count the number of valid signatures and revert if there aren't enough
+ if (_countValidSignatures(txHash, signatures, threshold) < threshold) revert InsufficientValidSignatures();
+ }
+
+ /**
+ * @notice Post-flight check to prevent `safe` signers from performing any of the following actions:
+ * 1. removing this contract guard
+ * 2. changing any modules
+ * 3. changing the threshold
+ * 4. changing the owners
+ * 5. changing the fallback handler
+ * CAUTION: If the safe has any authority over the signersHat(s) — i.e. wears their admin hat(s) or is an
+ * eligibility or toggle module — then in some cases protections (3) and (4) may not hold. Proceed with caution if
+ * considering granting such authority to the safe.
+ * @dev Modified from
+ * https://github.com/gnosis/zodiac-guard-mod/blob/988ebc7b71e352f121a0be5f6ae37e79e47a4541/contracts/ModGuard.sol#L86
+ */
+ function checkAfterExecution(bytes32, bool) public override {
+ // Ensure that this is only called in accordance with the Safe execTransaction flow
+ // And that it is not called from inside an execTransactionFromModule call
+ if (!_inSafeExecTransaction || _inModuleExecTransaction) revert NoReentryAllowed();
+
+ // module guard postflight check
+ if (guard != address(0)) {
+ BaseGuard(guard).checkAfterExecution(bytes32(0), false);
+ }
+
+ // reset the reentrancy guard to enable subsequent legitimate Safe execTransactions in the same transaction
+ _inSafeExecTransaction = false;
+
+ // if the transaction was a delegatecall, perform the post-flight check on the Safe state
+ // we don't need to check the Safe state for regular calls since the Safe state cannot be altered except by calling
+ // into the Safe, which is explicitly disallowed
+ if (_operation == Enum.Operation.DelegateCall) {
+ _checkSafeState(safe);
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ VIEW FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @inheritdoc IHatsSignerGate
+ function thresholdConfig() public view returns (ThresholdConfig memory) {
+ return _thresholdConfig;
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function isValidSigner(address _account) public view returns (bool valid) {
+ /// @dev existing `registeredSignerHats` are always valid, since `_validSignerHats` is append-only
+ /// We don't need a special case for `_account == address(0)` because the 0 hat id does not exist
+ valid = HATS.isWearerOfHat(_account, registeredSignerHats[_account]);
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function isValidSignerHat(uint256 _hatId) public view returns (bool valid) {
+ valid = _validSignerHats[_hatId];
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function validSignerCount() public view returns (uint256 signerCount) {
+ signerCount = _countValidSigners(safe.getOwners());
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function canAttachToSafe(ISafe _safe) public view returns (bool) {
+ return _safe.canAttachHSG();
+ }
+
+ /// @inheritdoc IHatsSignerGate
+ function getSafeDeployParamAddresses()
+ public
+ view
+ returns (
+ address _safeSingleton,
+ address _safeFallbackLibrary,
+ address _safeMultisendLibrary,
+ address _safeProxyFactory
+ )
+ {
+ return (SAFE_SINGLETON, SAFE_FALLBACK_LIBRARY, SAFE_MULTISEND_LIBRARY, SAFE_PROXY_FACTORY);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ INTERNAL HELPER FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @dev Internal function to set the owner hat
+ /// @param _ownerHat The hat id to set as the owner hat
+ function _setOwnerHat(uint256 _ownerHat) internal {
+ ownerHat = _ownerHat;
+ emit OwnerHatSet(_ownerHat);
+ }
+
+ /// @dev Internal function to approve new signer hats. Empty arrays and duplicate hats cause no harm, so they are
+ /// allowed.
+ /// @param _newSignerHats Array of hat ids to add as approved signer hats
+ function _addSignerHats(uint256[] memory _newSignerHats) internal {
+ for (uint256 i; i < _newSignerHats.length; ++i) {
+ _validSignerHats[_newSignerHats[i]] = true;
+ }
+
+ emit SignerHatsAdded(_newSignerHats);
+ }
+
+ /// @dev Internal function to set the threshold config
+ /// @param _config the new threshold config
+ function _setThresholdConfig(ThresholdConfig memory _config) internal {
+ // min threshold cannot be 0
+ if (_config.min == 0) revert InvalidThresholdConfig();
+
+ if (_config.thresholdType == TargetThresholdType.ABSOLUTE) {
+ // absolute target threshold cannot be lower than min threshold
+ if (_config.target < _config.min) revert InvalidThresholdConfig();
+ } else {
+ // proportional threshold cannot be greater than 100%
+ if (_config.target > 10_000) revert InvalidThresholdConfig();
+ }
+ // set the threshold config
+ _thresholdConfig = _config;
+
+ // log the change
+ emit ThresholdConfigSet(_config);
+ }
+
+ /// @dev Internal function to count the number of valid signers in an array of addresses. Does not check for
+ /// duplicates.
+ /// @param owners The addresses to check for validity
+ /// @return signerCount The number of valid signers in `owners`
+ function _countValidSigners(address[] memory owners) internal view returns (uint256 signerCount) {
+ // count the existing safe owners that wear the signer hat
+ for (uint256 i; i < owners.length; ++i) {
+ if (isValidSigner(owners[i])) {
+ // shouldn't overflow given reasonable owners array length
+ unchecked {
+ ++signerCount;
}
+ }
+ }
+ }
- /*
- We check the safe owner count in case there are existing owners who are no longer valid signers.
- If we're already at maxSigners, we'll replace one of the invalid owners by swapping the signer.
- Otherwise, we'll simply add the new signer.
- */
- uint256 ownerCount = owners.length;
- if (ownerCount >= maxSigs) {
- bool swapped = _swapSigner(owners, ownerCount, msg.sender);
- if (!swapped) {
- // if there are no invalid owners, we can't add a new signer, so we revert
- revert NoInvalidSignersToReplace();
- }
- } else {
- _grantSigner(owners, currentSignerCount, msg.sender);
+ /// @dev Counts the number of hats-valid signatures within a set of `signatures`
+ /// @dev modified from
+ /// https://github.com/safe-global/safe-contracts/blob/c36bcab46578a442862d043e12a83fec41143dec/contracts/Safe.sol#L240
+ /// @param dataHash The signed data
+ /// @param signatures The set of signatures to check
+ /// @param sigCount The number of signatures to check
+ /// @return validSigCount The number of hats-valid signatures
+ function _countValidSignatures(bytes32 dataHash, bytes memory signatures, uint256 sigCount)
+ internal
+ view
+ returns (uint256 validSigCount)
+ {
+ // There cannot be an owner with address 0.
+ address currentOwner;
+ uint8 v;
+ bytes32 r;
+ bytes32 s;
+ uint256 i;
+
+ for (i; i < sigCount; ++i) {
+ (v, r, s) = signatureSplit(signatures, i);
+ if (v == 0) {
+ // If v is 0 then it is a contract signature
+ // When handling contract signatures the address of the contract is encoded into r
+ currentOwner = address(uint160(uint256(r)));
+ } else if (v == 1) {
+ // If v is 1 then it is an approved hash
+ // When handling approved hashes the address of the approver is encoded into r
+ currentOwner = address(uint160(uint256(r)));
+ } else if (v > 30) {
+ // If v > 30 then default va (27,28) has been adjusted for eth_sign flow
+ // To support eth_sign and similar we adjust v and hash the messageHash with the Ethereum message prefix before
+ // applying ecrecover
+ currentOwner = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v - 4, r, s);
+ } else {
+ // Default is the ecrecover flow with the provided data hash
+ // Use ecrecover with the messageHash for EOA signatures
+ currentOwner = ecrecover(dataHash, v, r, s);
+ }
+
+ if (isValidSigner(currentOwner)) {
+ // shouldn't overflow given reasonable sigCount
+ unchecked {
+ ++validSigCount;
}
+ }
}
+ }
+
+ /// @dev Internal function to set the claimableFor parameter
+ /// @param _claimableFor Whether signer permissions are claimable on behalf of valid hat wearers
+ function _setClaimableFor(bool _claimableFor) internal {
+ claimableFor = _claimableFor;
+ emit ClaimableForSet(_claimableFor);
+ }
+
+ /// @dev Internal function to register a signer's hat if they are wearing a valid signer hat.
+ /// @param _hatToRegister The id of the hat to register
+ /// @param _signer The address to register
+ /// @param _allowReregistration Whether to allow registration of a different hat for an existing signer
+ function _registerSigner(uint256 _hatToRegister, address _signer, bool _allowReregistration) internal {
+ // check that the hat is valid
+ if (!isValidSignerHat(_hatToRegister)) revert InvalidSignerHat(_hatToRegister);
- /// @notice Checks if `_account` is a valid signer, ie is wearing the signer hat
- /// @dev Must be implemented by all flavors of HatsSignerGate
- /// @param _account The address to check
- /// @return valid Whether `_account` is a valid signer
- function isValidSigner(address _account) public view override returns (bool valid) {
- valid = HATS.isWearerOfHat(_account, signersHatId);
+ // check that the signer is wearing the hat
+ if (!HATS.isWearerOfHat(_signer, _hatToRegister)) revert NotSignerHatWearer(_signer);
+
+ // if specified, disallow re-registering a new hat for an existing signer that is still wearing their
+ // currently-registered hat
+ if (!_allowReregistration) {
+ if (HATS.isWearerOfHat(_signer, registeredSignerHats[_signer])) revert ReregistrationNotAllowed();
}
+
+ // register the hat used to claim. This will be the hat checked in `checkTransaction()` for this signer
+ registeredSignerHats[_signer] = _hatToRegister;
+
+ // log the registration
+ emit Registered(_hatToRegister, _signer);
+ }
+
+ /// @dev Internal function to add a `_signer` to the `safe` if they are not already an owner.
+ /// If this contract is the only owner on the `safe`, it will be swapped out for `_signer`. Otherwise, `_signer` will
+ /// be added as a new owner.
+ /// @param _signer The address to add as a new `safe` owner
+ function _addSigner(address _signer) internal {
+ ISafe s = safe;
+
+ // if the signer is not already an owner, add them
+ if (!s.isOwner(_signer)) {
+ // get the current owners
+ address[] memory owners = s.getOwners();
+
+ // initiate the addOwnerData, to be conditionally set below
+ bytes memory addOwnerData;
+
+ // if the only owner is this contract (set as an owner on initialization), replace it with _signer
+ if (owners.length == 1 && owners[0] == address(this)) {
+ // set up the swapOwner call
+ addOwnerData = SafeManagerLib.encodeSwapOwnerAction(SafeManagerLib.SENTINELS, address(this), _signer);
+ } else {
+ // update the threshold
+ uint256 newThreshold = _getNewThreshold(owners.length + 1);
+ // set up the addOwner call
+ addOwnerData = SafeManagerLib.encodeAddOwnerWithThresholdAction(_signer, newThreshold);
+ }
+
+ // execute the call
+ s.execSafeTransactionFromHSG(addOwnerData);
+ }
+ }
+
+ /// @dev Internal function to remove a signer from the `safe`, updating the threshold if appropriate
+ /// Unsafe. Does not check for signer validity before removal
+ /// @param _signer The address to remove
+ function _removeSigner(address _signer) internal {
+ ISafe s = safe;
+ bytes memory removeOwnerData;
+ address[] memory owners = s.getOwners();
+
+ delete registeredSignerHats[_signer];
+
+ if (owners.length == 1) {
+ // make address(this) the only owner
+ removeOwnerData = SafeManagerLib.encodeSwapOwnerAction(SafeManagerLib.SENTINELS, _signer, address(this));
+ } else {
+ // update the threshold
+ uint256 newThreshold = _getNewThreshold(owners.length - 1);
+
+ removeOwnerData =
+ SafeManagerLib.encodeRemoveOwnerAction(SafeManagerLib.findPrevOwner(owners, _signer), _signer, newThreshold);
+ }
+
+ // execute the call
+ s.execSafeTransactionFromHSG(removeOwnerData);
+ }
+
+ /// @dev Internal function to calculate the required amount of valid signatures according to the current number of
+ /// owners in the safe and the threshold config
+ /// @param numOwners The number of owners in the safe
+ /// @return _requiredValidSignatures The required amount of valid signatures
+ function _getRequiredValidSignatures(uint256 numOwners) internal view returns (uint256 _requiredValidSignatures) {
+ // get the threshold config
+ ThresholdConfig memory config = _thresholdConfig;
+
+ // calculate the correct threshold
+ if (config.thresholdType == TargetThresholdType.ABSOLUTE) {
+ // ABSOLUTE
+ if (numOwners < config.min) _requiredValidSignatures = config.min;
+ else if (numOwners > config.target) _requiredValidSignatures = config.target;
+ else _requiredValidSignatures = numOwners;
+ } else {
+ // PROPORTIONAL
+ // add 9999 to round up
+ _requiredValidSignatures = ((numOwners * config.target) + 9999) / 10_000;
+ // ensure that the threshold is not lower than the min threshold
+ if (_requiredValidSignatures < config.min) _requiredValidSignatures = config.min;
+ }
+ }
+
+ /// @dev Internal function to get the safe's threshold according to the current number of owners and the threshold
+ /// config. The threshold is always the minimum between the required amount of valid signatures and the number of
+ /// owners
+ /// @param numOwners The number of owners in the safe
+ /// @return _threshold The safe's threshold
+ function _getNewThreshold(uint256 numOwners) internal view returns (uint256 _threshold) {
+ // get the required amount of valid signatures according to the current number of owners and the threshold config
+ _threshold = _getRequiredValidSignatures(numOwners);
+ // the threshold cannot be higher than the number of owners
+ if (_threshold > numOwners) {
+ _threshold = numOwners;
+ }
+ }
+
+ /// @dev Locks the contract, preventing any further owner changes
+ function _lock() internal {
+ locked = true;
+ emit HSGLocked();
+ }
+
+ /// @dev Internal function to set a delegatecall target
+ /// @param _target The address to set
+ /// @param _enabled Whether to enable or disable the target
+ function _setDelegatecallTarget(address _target, bool _enabled) internal {
+ enabledDelegatecallTargets[_target] = _enabled;
+ emit DelegatecallTargetEnabled(_target, _enabled);
+ }
+
+ // solhint-disallow-next-line payable-fallback
+ fallback() external {
+ // We don't revert on fallback to avoid issues in case of a Safe upgrade
+ // E.g. The expected check method might change and then the Safe would be locked.
+ }
+
+ // /*//////////////////////////////////////////////////////////////
+ // ZODIAC MODIFIER FUNCTIONS
+ // //////////////////////////////////////////////////////////////*/
+
+ /// @notice Allows a module to execute a call from the context of the Safe. Modules are not allowed to...
+ /// - delegatecall to unapproved targets
+ /// - change any Safe state, whether via a delegatecall to an approved target or a direct call
+ /// @dev Can only be called by an enabled module.
+ /// @dev Must emit ExecutionFromModuleSuccess(address module) if successful.
+ /// @dev Must emit ExecutionFromModuleFailure(address module) if unsuccessful.
+ /// @param to Destination address of module transaction.
+ /// @param value Ether value of module transaction.
+ /// @param data Data payload of module transaction.
+ /// @param operation Operation type of module transaction: 0 == call, 1 == delegate call.
+ function execTransactionFromModule(address to, uint256 value, bytes calldata data, Enum.Operation operation)
+ public
+ override
+ moduleOnly
+ returns (bool success)
+ {
+ // perform pre-flight checks
+ ISafe s = _beforeExecTransactionFromModule(to, operation);
+
+ // forward the call to the safe
+ success = s.execTransactionFromModule(to, value, data, operation);
+
+ // perform post-flight checks and emit event
+ _afterExecTransactionFromModule(success, operation, s);
+ }
+
+ /// @notice Allows a module to execute a call from the context of the Safe. Modules are not allowed to...
+ /// - delegatecall to unapproved targets
+ /// - change any Safe state, whether via a delegatecall to an approved target or a direct call
+ /// @dev Can only be called by an enabled module.
+ /// @dev Must emit ExecutionFromModuleSuccess(address module) if successful.
+ /// @dev Must emit ExecutionFromModuleFailure(address module) if unsuccessful.
+ /// @param to Destination address of module transaction.
+ /// @param value Ether value of module transaction.
+ /// @param data Data payload of module transaction.
+ /// @param operation Operation type of module transaction: 0 == call, 1 == delegate call.
+ function execTransactionFromModuleReturnData(address to, uint256 value, bytes calldata data, Enum.Operation operation)
+ public
+ override
+ moduleOnly
+ returns (bool success, bytes memory returnData)
+ {
+ // perform pre-flight checks
+ ISafe s = _beforeExecTransactionFromModule(to, operation);
+
+ // forward the call to the safe
+ (success, returnData) = s.execTransactionFromModuleReturnData(to, value, data, operation);
+
+ // perform post-flight checks and emit event
+ _afterExecTransactionFromModule(success, operation, s);
+ }
+
+ /// @inheritdoc ModifierUnowned
+ /// @dev Only callable by a wearer of the owner hat, and only if the contract is not locked.
+ function disableModule(address prevModule, address module) public override {
+ _checkUnlocked();
+ _checkOwner();
+ super.disableModule(prevModule, module);
+ }
+
+ /// @notice Enables a module that can add transactions to the queue
+ /// @dev Only callable by a wearer of the owner hat, and only if the contract is not locked.
+ /// @param module Address of the module to be enabled
+ function enableModule(address module) public {
+ _checkUnlocked();
+ _checkOwner();
+ _enableModule(module);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ ZODIAC GUARD FUNCTION
+ //////////////////////////////////////////////////////////////*/
+
+ /// @notice Set a guard that checks transactions before execution.
+ /// @dev Only callable by a wearer of the owner hat, and only if the contract is not locked.
+ /// @param _guard The address of the guard to be used or the 0 address to disable the guard.
+ function setGuard(address _guard) public {
+ _checkUnlocked();
+ _checkOwner();
+ _setGuard(_guard);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ INTERNAL ZODIAC HELPER FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @dev Internal function to check that a module transaction is valid. Modules are not allowed to...
+ /// - delegatecall to unapproved targets
+ /// - change any Safe state via a delegatecall to an approved target
+ /// - call the safe directly (prevents Safe state changes)
+ /// @param _to The address of the target of the module transaction
+ /// @param operation_ The operation type of the module transaction
+ /// @param _safe The safe that is executing the module transaction
+ function _checkModuleTransaction(address _to, Enum.Operation operation_, ISafe _safe) internal {
+ // preflight checks
+ if (operation_ == Enum.Operation.DelegateCall) {
+ // case: DELEGATECALL
+ // We disallow delegatecalls to unapproved targets
+ if (!enabledDelegatecallTargets[_to]) revert DelegatecallTargetNotEnabled();
+
+ // If the delegatecall target is approved, we record the existing owners, threshold, and fallback handler for
+ // post-flight check
+ _existingOwnersHash = keccak256(abi.encode(_safe.getOwners()));
+ _existingThreshold = _safe.getThreshold();
+ _existingFallbackHandler = _safe.getSafeFallbackHandler();
+ } else if (_to == address(_safe)) {
+ // case: CALL to the safe
+ // We disallow external calls to the safe itself. Together with the above check, this ensure there are no
+ // unauthorized calls into the Safe itself
+ revert CannotCallSafe();
+ }
+
+ // case: CALL to a non-Safe target
+ // Return and proceed to subsequent logic
+ }
+
+ /// @dev Internal function to check that a delegatecall executed by the signers or a module do not change the
+ /// `_safe`'s
+ /// state.
+ function _checkSafeState(ISafe _safe) internal view {
+ if (_safe.getSafeGuard() != address(this)) revert CannotDisableThisGuard();
+
+ // prevent signers from changing the threshold
+ if (_safe.getThreshold() != _existingThreshold) revert CannotChangeThreshold();
+
+ // prevent signers from changing the owners
+ if (keccak256(abi.encode(_safe.getOwners())) != _existingOwnersHash) revert CannotChangeOwners();
+
+ // prevent changes to the fallback handler
+ if (_safe.getSafeFallbackHandler() != _existingFallbackHandler) revert CannotChangeFallbackHandler();
+
+ // prevent signers from removing this module or adding any other modules
+ (address[] memory modulesWith1, address next) = _safe.getModulesWith1();
+
+ // ensure that there is only one module...
+ // if the length is 0, we know this module has been removed
+ // per Safe ModuleManager.sol#137, "If all entries fit into a single page, the next pointer will be 0x1", ie
+ // SENTINELS. Therefore, if `next` is not SENTINELS, we know another module has been added.
+ // We also check that the only module is this contract
+ if (modulesWith1.length == 0 || next != SafeManagerLib.SENTINELS || modulesWith1[0] != address(this)) {
+ revert CannotChangeModules();
+ }
+ }
+
+ /// @dev Internal function to run the pre-flight checks for execTransactionFromModule and
+ /// execTransactionFromModuleReturnData
+ /// @param _to The address of the target of the module transaction
+ /// @param operation_ The operation type of the module transaction
+ /// @return _safe The safe that is executing the module transaction
+ function _beforeExecTransactionFromModule(address _to, Enum.Operation operation_) internal returns (ISafe _safe) {
+ // Disallow entering this function from inside a Safe execTransaction call or another execTransactionFromModule call
+ if (_inSafeExecTransaction || _inModuleExecTransaction) revert NoReentryAllowed();
+
+ // set the reentrancy guard
+ _inModuleExecTransaction = true;
+
+ _safe = safe;
+
+ // preflight checks
+ _checkModuleTransaction(_to, operation_, _safe);
+ }
+
+ /// @dev Internal function to emit the appropriate execution status event and run the post-flight checks for
+ /// execTransactionFromModule and execTransactionFromModuleReturnData
+ /// @param _success The success status of the module transaction
+ /// @param operation_ The operation type of the module transaction
+ /// @param _safe The safe that is executing the module transaction
+ function _afterExecTransactionFromModule(bool _success, Enum.Operation operation_, ISafe _safe) internal {
+ // emit the appropriate execution status event
+ if (_success) {
+ emit ExecutionFromModuleSuccess(msg.sender);
+ } else {
+ emit ExecutionFromModuleFailure(msg.sender);
+ }
+
+ // Ensure that the Safe state is not altered by delegatecalls. We don't need to check the Safe state for regular
+ // calls since the Safe state cannot be altered except by calling into the Safe, which is explicitly disallowed.
+ if (operation_ == Enum.Operation.DelegateCall) _checkSafeState(_safe);
+
+ // reset the reentrancy guard to enable future legitimate calls within the same transaction
+ _inModuleExecTransaction = false;
+ }
}
diff --git a/src/HatsSignerGateBase.sol b/src/HatsSignerGateBase.sol
deleted file mode 100644
index 53ad8a8..0000000
--- a/src/HatsSignerGateBase.sol
+++ /dev/null
@@ -1,579 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity >=0.8.13;
-
-// import { Test, console2 } from "forge-std/Test.sol"; // remove after testing
-import "./HSGLib.sol";
-import { HatsOwnedInitializable } from "hats-auth/HatsOwnedInitializable.sol";
-import { BaseGuard } from "zodiac/guard/BaseGuard.sol";
-import { IAvatar } from "zodiac/interfaces/IAvatar.sol";
-import { StorageAccessible } from "@gnosis.pm/safe-contracts/contracts/common/StorageAccessible.sol";
-import { IGnosisSafe, Enum } from "./Interfaces/IGnosisSafe.sol";
-import { SignatureDecoder } from "@gnosis.pm/safe-contracts/contracts/common/SignatureDecoder.sol";
-
-abstract contract HatsSignerGateBase is BaseGuard, SignatureDecoder, HatsOwnedInitializable {
- /// @notice The multisig to which this contract is attached
- IGnosisSafe public safe;
-
- /// @notice The minimum signature threshold for the `safe`
- uint256 public minThreshold;
-
- /// @notice The highest level signature threshold for the `safe`
- uint256 public targetThreshold;
-
- /// @notice The maximum number of signers allowed for the `safe`
- uint256 public maxSigners;
-
- /// @notice The version of HatsSignerGate used in this contract
- string public version;
-
- /// @dev Temporary record of the existing owners on the `safe` when a transaction is submitted
- bytes32 internal _existingOwnersHash;
-
- /// @dev A simple re-entrency guard
- uint256 internal _guardEntries;
-
- /// @dev The head pointer used in the GnosisSafe owners linked list, as well as the module linked list
- address internal constant SENTINEL_OWNERS = address(0x1);
-
- /// @dev The storage slot used by GnosisSafe to store the guard address
- /// keccak256("guard_manager.guard.address")
- bytes32 internal constant GUARD_STORAGE_SLOT = 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8;
-
- /// @dev Makes the singleton unusable by setting its owner to the 1-address
- constructor() payable initializer {
- _HatsOwned_init(1, address(0x1));
- }
-
- /// @notice Initializes a new instance
- /// @dev Can only be called once
- /// @param initializeParams ABI-encoded bytes with initialization parameters
- function setUp(bytes calldata initializeParams) public payable virtual initializer { }
-
- /// @notice Internal function to initialize a new instance
- /// @param _ownerHatId The hat id of the hat that owns this instance of HatsSignerGate
- /// @param _safe The multisig to which this instance of HatsSignerGate is attached
- /// @param _hats The Hats Protocol address
- /// @param _minThreshold The minimum threshold for the `_safe`
- /// @param _targetThreshold The maxium threshold for the `_safe`
- /// @param _maxSigners The maximum number of signers allowed on the `_safe`
- /// @param _version The current version of HatsSignerGate
- function _setUp(
- uint256 _ownerHatId,
- address _safe,
- address _hats,
- uint256 _minThreshold,
- uint256 _targetThreshold,
- uint256 _maxSigners,
- string memory _version
- ) internal {
- _HatsOwned_init(_ownerHatId, _hats);
- maxSigners = _maxSigners;
- safe = IGnosisSafe(_safe);
-
- _setTargetThreshold(_targetThreshold);
- _setMinThreshold(_minThreshold);
- version = _version;
- }
-
- /// @notice Checks if `_account` is a valid signer
- /// @dev Must be implemented by all flavors of HatsSignerGate
- /// @param _account The address to check
- /// @return valid Whether `_account` is a valid signer
- function isValidSigner(address _account) public view virtual returns (bool valid) { }
-
- /// @notice Sets a new target threshold, and changes `safe`'s threshold if appropriate
- /// @dev Only callable by a wearer of the owner hat. Reverts if `_targetThreshold` is greater than `maxSigners`.
- /// @param _targetThreshold The new target threshold to set
- function setTargetThreshold(uint256 _targetThreshold) public onlyOwner {
- if (_targetThreshold != targetThreshold) {
- _setTargetThreshold(_targetThreshold);
-
- uint256 signerCount = validSignerCount();
- if (signerCount > 1) _setSafeThreshold(_targetThreshold, signerCount);
-
- emit HSGLib.TargetThresholdSet(_targetThreshold);
- }
- }
-
- /// @notice Internal function to set the target threshold
- /// @dev Reverts if `_targetThreshold` is greater than `maxSigners` or lower than `minThreshold`
- /// @param _targetThreshold The new target threshold to set
- function _setTargetThreshold(uint256 _targetThreshold) internal {
- // target threshold cannot be lower than min threshold
- if (_targetThreshold < minThreshold) {
- revert InvalidTargetThreshold();
- }
- // target threshold cannot be greater than max signers
- if (_targetThreshold > maxSigners) {
- revert InvalidTargetThreshold();
- }
-
- targetThreshold = _targetThreshold;
- }
-
- /// @notice Internal function to set the threshold for the `safe`
- /// @dev Forwards the threshold-setting call to `safe.ExecTransactionFromModule`
- /// @param _threshold The threshold to set on the `safe`
- /// @param _signerCount The number of valid signers on the `safe`; should be calculated from `validSignerCount()`
- function _setSafeThreshold(uint256 _threshold, uint256 _signerCount) internal {
- uint256 newThreshold = _threshold;
-
- // ensure that txs can't execute if fewer signers than target threshold
- if (_signerCount <= _threshold) {
- newThreshold = _signerCount;
- }
- if (newThreshold != safe.getThreshold()) {
- bytes memory data = abi.encodeWithSignature("changeThreshold(uint256)", newThreshold);
-
- bool success = safe.execTransactionFromModule(
- address(safe), // to
- 0, // value
- data, // data
- Enum.Operation.Call // operation
- );
-
- if (!success) {
- revert FailedExecChangeThreshold();
- }
- }
- }
-
- /// @notice Sets a new minimum threshold
- /// @dev Only callable by a wearer of the owner hat. Reverts if `_minThreshold` is greater than `maxSigners` or `targetThreshold`
- /// @param _minThreshold The new minimum threshold
- function setMinThreshold(uint256 _minThreshold) public onlyOwner {
- _setMinThreshold(_minThreshold);
- emit HSGLib.MinThresholdSet(_minThreshold);
- }
-
- /// @notice Internal function to set a new minimum threshold
- /// @dev Only callable by a wearer of the owner hat. Reverts if `_minThreshold` is greater than `maxSigners` or `targetThreshold`
- /// @param _minThreshold The new minimum threshold
- function _setMinThreshold(uint256 _minThreshold) internal {
- if (_minThreshold > maxSigners || _minThreshold > targetThreshold) {
- revert InvalidMinThreshold();
- }
-
- minThreshold = _minThreshold;
- }
-
- /// @notice Tallies the number of existing `safe` owners that wear a signer hat and updates the `safe` threshold if necessary
- /// @dev Does NOT remove invalid `safe` owners
- function reconcileSignerCount() public {
- uint256 signerCount = validSignerCount();
-
- if (signerCount > maxSigners) {
- revert MaxSignersReached();
- }
-
- uint256 currentThreshold = safe.getThreshold();
- uint256 newThreshold;
- uint256 target = targetThreshold; // save SLOADs
-
- if (signerCount <= target && signerCount != currentThreshold) {
- newThreshold = signerCount;
- } else if (signerCount > target && currentThreshold < target) {
- newThreshold = target;
- }
- if (newThreshold > 0) {
- bytes memory data = abi.encodeWithSignature("changeThreshold(uint256)", newThreshold);
-
- bool success = safe.execTransactionFromModule(
- address(safe), // to
- 0, // value
- data, // data
- Enum.Operation.Call // operation
- );
-
- if (!success) {
- revert FailedExecChangeThreshold();
- }
- }
- }
-
- /// @notice Tallies the number of existing `safe` owners that wear a signer hat
- /// @return signerCount The number of valid signers on the `safe`
- function validSignerCount() public view returns (uint256 signerCount) {
- signerCount = _countValidSigners(safe.getOwners());
- }
-
- /// @notice Internal function to count the number of valid signers in an array of addresses
- /// @param owners The addresses to check for validity
- /// @return signerCount The number of valid signers in `owners`
- function _countValidSigners(address[] memory owners) internal view returns (uint256 signerCount) {
- uint256 length = owners.length;
- // count the existing safe owners that wear the signer hat
- for (uint256 i; i < length;) {
- if (isValidSigner(owners[i])) {
- // shouldn't overflow given reasonable owners array length
- unchecked {
- ++signerCount;
- }
- }
- // shouldn't overflow given reasonable owners array length
- unchecked {
- ++i;
- }
- }
- }
-
- /// @notice Internal function that adds `_signer` as an owner on `safe`, updating the threshold if appropriate
- /// @dev Unsafe. Does not check if `_signer` is a valid signer
- /// @param _owners Array of owners on the `safe`
- /// @param _currentSignerCount The current number of signers
- /// @param _signer The address to add as a new `safe` owner
- function _grantSigner(address[] memory _owners, uint256 _currentSignerCount, address _signer) internal {
- uint256 newSignerCount = _currentSignerCount;
-
- uint256 currentThreshold = safe.getThreshold(); // view function
- uint256 newThreshold = currentThreshold;
-
- bytes memory addOwnerData;
-
- // if the only owner is a non-signer (ie this module set as an owner on initialization), replace it with _signer
- if (_owners.length == 1 && _owners[0] == address(this)) {
- // prevOwner will always be the sentinel when owners.length == 1
-
- // set up the swapOwner call
- addOwnerData = abi.encodeWithSignature(
- "swapOwner(address,address,address)",
- SENTINEL_OWNERS, // prevOwner
- address(this), // oldOwner
- _signer // newOwner
- );
- unchecked {
- // shouldn't overflow given MaxSignersReached check higher in call stack
- ++newSignerCount;
- }
- } else {
- // otherwise, add the claimer as a new owner
-
- unchecked {
- // shouldn't overflow given MaxSignersReached check higher in call stack
- ++newSignerCount;
- }
-
- // ensure that txs can't execute if fewer signers than target threshold
- if (newSignerCount <= targetThreshold) {
- newThreshold = newSignerCount;
- }
-
- // set up the addOwner call
- addOwnerData = abi.encodeWithSignature("addOwnerWithThreshold(address,uint256)", _signer, newThreshold);
- }
-
- // execute the call
- bool success = safe.execTransactionFromModule(
- address(safe), // to
- 0, // value
- addOwnerData, // data
- Enum.Operation.Call // operation
- );
-
- if (!success) {
- revert FailedExecAddSigner();
- }
- }
-
- /// @notice Internal function that adds `_signer` as an owner on `safe` by swapping with an existing (invalid) owner
- /// @dev Unsafe. Does not check if `_signer` is a valid signer.
- /// @param _owners Array of owners on the `safe`
- /// @param _ownerCount The number of owners on the `safe` (length of `_owners` array)
- /// @param _signer The address to add as a new `safe` owner
- /// @return success Whether an invalid signer was found and successfully replaced with `_signer`
- function _swapSigner(address[] memory _owners, uint256 _ownerCount, address _signer)
- internal
- returns (bool success)
- {
- address ownerToCheck;
- bytes memory data;
-
- for (uint256 i; i < _ownerCount;) {
- ownerToCheck = _owners[i];
-
- if (!isValidSigner(ownerToCheck)) {
- // prep the swap
- data = abi.encodeWithSignature(
- "swapOwner(address,address,address)",
- _findPrevOwner(_owners, ownerToCheck), // prevOwner
- ownerToCheck, // oldOwner
- _signer // newOwner
- );
-
- // execute the swap, reverting if it fails for some reason
- success = safe.execTransactionFromModule(
- address(safe), // to
- 0, // value
- data, // data
- Enum.Operation.Call // operation
- );
-
- if (!success) {
- revert FailedExecRemoveSigner();
- }
-
- break;
- }
- unchecked {
- ++i;
- }
- }
- }
-
- /// @notice Removes an invalid signer from the `safe`, updating the threshold if appropriate
- /// @param _signer The address to remove if not a valid signer
- function removeSigner(address _signer) public virtual {
- if (isValidSigner(_signer)) {
- revert StillWearsSignerHat(_signer);
- }
-
- _removeSigner(_signer);
- }
-
- /// @notice Internal function to remove a signer from the `safe`, updating the threshold if appropriate
- /// @dev Unsafe. Does not check for signer validity before removal
- /// @param _signer The address to remove
- function _removeSigner(address _signer) internal {
- bytes memory removeOwnerData;
- address[] memory owners = safe.getOwners();
- uint256 validSigners = _countValidSigners(owners);
- // uint256 newSignerCount;
-
- if (validSigners < 2 && owners.length == 1) {
- // signerCount could be 0 after reconcileSignerCount
- // make address(this) the only owner
- removeOwnerData = abi.encodeWithSignature(
- "swapOwner(address,address,address)",
- SENTINEL_OWNERS, // prevOwner
- _signer, // oldOwner
- address(this) // newOwner
- );
-
- // newSignerCount is already 0
- } else {
- uint256 currentThreshold = safe.getThreshold();
- uint256 newThreshold = currentThreshold;
- // uint256 validSignerCount = _countValidSigners(owners);
-
- // ensure that txs can't execute if fewer signers than target threshold
- if (validSigners <= targetThreshold) {
- newThreshold = validSigners;
- }
-
- removeOwnerData = abi.encodeWithSignature(
- "removeOwner(address,address,uint256)", _findPrevOwner(owners, _signer), _signer, newThreshold
- );
- }
-
- bool success = safe.execTransactionFromModule(
- address(safe), // to
- 0, // value
- removeOwnerData, // data
- Enum.Operation.Call // operation
- );
-
- if (!success) {
- revert FailedExecRemoveSigner();
- }
- }
-
- /// @notice Internal function to find the previous owner of an `_owner` in an array of `_owners`, ie the pointer to the owner to remove from the `safe` owners linked list
- /// @param _owners An array of addresses
- /// @param _owner The address after the one to find
- /// @return prevOwner The owner previous to `_owner` in the `safe` linked list
- function _findPrevOwner(address[] memory _owners, address _owner) internal pure returns (address prevOwner) {
- prevOwner = SENTINEL_OWNERS;
-
- for (uint256 i; i < _owners.length;) {
- if (_owners[i] == _owner) {
- if (i == 0) break;
- prevOwner = _owners[i - 1];
- }
- // shouldn't overflow given reasonable _owners array length
- unchecked {
- ++i;
- }
- }
- }
-
- // solhint-disallow-next-line payable-fallback
- fallback() external {
- // We don't revert on fallback to avoid issues in case of a Safe upgrade
- // E.g. The expected check method might change and then the Safe would be locked.
- }
-
- /// @notice Pre-flight check on a `safe` transaction to ensure that it s signers are valid, called from within `safe.execTransactionFromModule()`
- /// @dev Overrides All params mirror params for `safe.execTransactionFromModule()`
- function checkTransaction(
- address to,
- uint256 value,
- bytes calldata data,
- Enum.Operation operation,
- uint256 safeTxGas,
- uint256 baseGas,
- uint256 gasPrice,
- address gasToken,
- address payable refundReceiver,
- bytes memory signatures,
- address // msgSender
- ) external override {
- if (msg.sender != address(safe)) revert NotCalledFromSafe();
- // get the safe owners
- address[] memory owners = safe.getOwners();
- {
- // scope to avoid stack too deep errors
- uint256 safeOwnerCount = owners.length;
- // uint256 validSignerCount = _countValidSigners(safe.getOwners());
- // ensure that safe threshold is correct
- reconcileSignerCount();
-
- if (safeOwnerCount < minThreshold) {
- revert BelowMinThreshold(minThreshold, safeOwnerCount);
- }
- }
- // get the tx hash; view function
- bytes32 txHash = safe.getTransactionHash(
- // Transaction info
- to,
- value,
- data,
- operation,
- safeTxGas,
- // Payment info
- baseGas,
- gasPrice,
- gasToken,
- refundReceiver,
- // Signature info
- // We subtract 1 since nonce was just incremented in the parent function call
- safe.nonce() - 1 // view function
- );
- uint256 threshold = safe.getThreshold();
- uint256 validSigCount = countValidSignatures(txHash, signatures, threshold);
-
- // revert if there aren't enough valid signatures
- if (validSigCount < threshold || validSigCount < minThreshold) {
- revert InvalidSigners();
- }
-
- // record existing owners for post-flight check
- _existingOwnersHash = keccak256(abi.encode(owners));
-
- unchecked {
- ++_guardEntries;
- }
- // revert if re-entry is detected
- if (_guardEntries > 1) revert NoReentryAllowed();
- }
-
- /**
- * @notice Post-flight check to prevent `safe` signers from performing any of the following actions:
- * 1. removing this contract guard
- * 2. changing any modules
- * 3. changing the threshold
- * 4. changing the owners
- * CAUTION: If the safe has any authority over the signersHat(s) — i.e. wears their admin hat(s) or is an eligibility or toggle module —
- * then in some cases protections (3) and (4) may not hold. Proceed with caution if considering granting such authority to the safe.
- * @dev Modified from https://github.com/gnosis/zodiac-guard-mod/blob/988ebc7b71e352f121a0be5f6ae37e79e47a4541/contracts/ModGuard.sol#L86
- */
- function checkAfterExecution(bytes32, bool) external override {
- if (msg.sender != address(safe)) revert NotCalledFromSafe();
- // prevent signers from disabling this guard
- if (
- abi.decode(StorageAccessible(address(safe)).getStorageAt(uint256(GUARD_STORAGE_SLOT), 1), (address))
- != address(this)
- ) {
- revert CannotDisableThisGuard(address(this));
- }
- // prevent signers from changing the threshold
- if (safe.getThreshold() != _getCorrectThreshold()) {
- revert SignersCannotChangeThreshold();
- }
- // prevent signers from changing the owners
- address[] memory owners = safe.getOwners();
- if (keccak256(abi.encode(owners)) != _existingOwnersHash) {
- revert SignersCannotChangeOwners();
- }
- // prevent signers from removing this module or adding any other modules
- /// @dev SENTINEL_OWNERS and SENTINEL_MODULES are both address(0x1)
- (address[] memory modulesWith1, address next) = safe.getModulesPaginated(SENTINEL_OWNERS, 1);
- // ensure that there is only one module...
- if (
- // if the length is 0, we know this module has been removed
- // forgefmt: disable-next-line
- modulesWith1.length == 0
- /* per Safe ModuleManager.sol#137, "If all entries fit into a single page, the next pointer will be 0x1", ie SENTINEL_OWNERS.
- Therefore, if `next` is not SENTINEL_OWNERS, we know another module has been added. */
- || next != SENTINEL_OWNERS
- ) {
- revert SignersCannotChangeModules();
- } // ...and that the only module is this contract
- else if (modulesWith1[0] != address(this)) {
- revert SignersCannotChangeModules();
- }
- // leave checked to catch underflows triggered by re-entry attempts
- --_guardEntries;
- }
-
- /// @notice Internal function to calculate the threshold that `safe` should have, given the correct `signerCount`, `minThreshold`, and `targetThreshold`
- /// @return _threshold The correct threshold
- function _getCorrectThreshold() internal view returns (uint256 _threshold) {
- uint256 count = validSignerCount();
- uint256 min = minThreshold;
- uint256 max = targetThreshold;
- if (count < min) _threshold = min;
- else if (count > max) _threshold = max;
- else _threshold = count;
- }
-
- /// @notice Counts the number of hats-valid signatures within a set of `signatures`
- /// @dev modified from https://github.com/safe-global/safe-contracts/blob/c36bcab46578a442862d043e12a83fec41143dec/contracts/GnosisSafe.sol#L240
- /// @param dataHash The signed data
- /// @param signatures The set of signatures to check
- /// @return validSigCount The number of hats-valid signatures
- function countValidSignatures(bytes32 dataHash, bytes memory signatures, uint256 sigCount)
- public
- view
- returns (uint256 validSigCount)
- {
- // There cannot be an owner with address 0.
- address currentOwner;
- uint8 v;
- bytes32 r;
- bytes32 s;
- uint256 i;
-
- for (i; i < sigCount;) {
- (v, r, s) = signatureSplit(signatures, i);
- if (v == 0) {
- // If v is 0 then it is a contract signature
- // When handling contract signatures the address of the contract is encoded into r
- currentOwner = address(uint160(uint256(r)));
- } else if (v == 1) {
- // If v is 1 then it is an approved hash
- // When handling approved hashes the address of the approver is encoded into r
- currentOwner = address(uint160(uint256(r)));
- } else if (v > 30) {
- // If v > 30 then default va (27,28) has been adjusted for eth_sign flow
- // To support eth_sign and similar we adjust v and hash the messageHash with the Ethereum message prefix before applying ecrecover
- currentOwner =
- ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v - 4, r, s);
- } else {
- // Default is the ecrecover flow with the provided data hash
- // Use ecrecover with the messageHash for EOA signatures
- currentOwner = ecrecover(dataHash, v, r, s);
- }
-
- if (isValidSigner(currentOwner)) {
- // shouldn't overflow given reasonable sigCount
- unchecked {
- ++validSigCount;
- }
- }
- // shouldn't overflow given reasonable sigCount
- unchecked {
- ++i;
- }
- }
- }
-}
diff --git a/src/HatsSignerGateFactory.sol b/src/HatsSignerGateFactory.sol
deleted file mode 100644
index f99f479..0000000
--- a/src/HatsSignerGateFactory.sol
+++ /dev/null
@@ -1,303 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity >=0.8.13;
-
-// import { console2 } from "forge-std/Test.sol"; // remove after testing
-import "./HatsSignerGate.sol";
-import "./MultiHatsSignerGate.sol";
-import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";
-import "@gnosis.pm/safe-contracts/contracts/libraries/MultiSend.sol";
-import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol";
-import "@gnosis.pm/zodiac/factory/ModuleProxyFactory.sol";
-
-contract HatsSignerGateFactory {
- /// @notice (Multi)HatsSignerGates cannot be used with other modules
- error NoOtherModulesAllowed();
-
- address public immutable hatsAddress;
-
- address public immutable hatsSignerGateSingleton;
- address public immutable multiHatsSignerGateSingleton;
-
- // address public immutable hatsSignerGatesingleton;
- address public immutable safeSingleton;
-
- // Library to use for EIP1271 compatability
- address public immutable gnosisFallbackLibrary;
-
- // Library to use for all safe transaction executions
- address public immutable gnosisMultisendLibrary;
-
- GnosisSafeProxyFactory public immutable gnosisSafeProxyFactory;
-
- ModuleProxyFactory public immutable moduleProxyFactory;
-
- string public version;
-
- uint256 internal nonce;
-
- address internal constant SENTINEL_MODULES = address(0x1);
-
- // events
-
- event HatsSignerGateSetup(
- address _hatsSignerGate,
- uint256 _ownerHatId,
- uint256 _signersHatId,
- address _safe,
- uint256 _minThreshold,
- uint256 _targetThreshold,
- uint256 _maxSigners
- );
-
- event MultiHatsSignerGateSetup(
- address _hatsSignerGate,
- uint256 _ownerHatId,
- uint256[] _signersHatIds,
- address _safe,
- uint256 _minThreshold,
- uint256 _targetThreshold,
- uint256 _maxSigners
- );
-
- constructor(
- address _hatsSignerGateSingleton,
- address _multiHatsSignerGateSingleton,
- address _hatsAddress,
- address _safeSingleton,
- address _gnosisFallbackLibrary,
- address _gnosisMultisendLibrary,
- address _gnosisSafeProxyFactory,
- address _moduleProxyFactory,
- string memory _version
- ) {
- hatsSignerGateSingleton = _hatsSignerGateSingleton;
- multiHatsSignerGateSingleton = _multiHatsSignerGateSingleton;
- hatsAddress = _hatsAddress;
- safeSingleton = _safeSingleton;
- gnosisFallbackLibrary = _gnosisFallbackLibrary;
- gnosisMultisendLibrary = _gnosisMultisendLibrary;
- gnosisSafeProxyFactory = GnosisSafeProxyFactory(_gnosisSafeProxyFactory);
- moduleProxyFactory = ModuleProxyFactory(_moduleProxyFactory);
- version = _version;
- }
-
- /// @notice Deploy a new HatsSignerGate and a new Safe, all wired up together
- function deployHatsSignerGateAndSafe(
- uint256 _ownerHatId,
- uint256 _signersHatId,
- uint256 _minThreshold,
- uint256 _targetThreshold,
- uint256 _maxSigners
- ) public returns (address hsg, address payable safe) {
- // Deploy new safe but do not set it up yet
- safe = payable(gnosisSafeProxyFactory.createProxy(safeSingleton, hex"00"));
-
- // Deploy new hats signer gate
- hsg = _deployHatsSignerGate(_ownerHatId, _signersHatId, safe, _minThreshold, _targetThreshold, _maxSigners);
-
- // Generate delegate call so the safe calls enableModule on itself during setup
- bytes memory multisendAction = _generateMultisendAction(hsg, safe);
-
- // Workaround for solidity dynamic memory array
- address[] memory owners = new address[](1);
- owners[0] = hsg;
-
- // Call setup on safe to enable our new module/guard and set it as the sole initial owner
- GnosisSafe(safe).setup(
- owners,
- 1,
- gnosisMultisendLibrary,
- multisendAction, // set hsg as module and guard
- gnosisFallbackLibrary,
- address(0),
- 0,
- payable(address(0))
- );
-
- emit HatsSignerGateSetup(hsg, _ownerHatId, _signersHatId, safe, _minThreshold, _targetThreshold, _maxSigners);
-
- return (hsg, safe);
- }
-
- /**
- * @notice Deploy a new HatsSignerGate and relate it to an existing Safe
- * @dev In order to wire it up to the existing Safe, the owners of the Safe must enable it as a module and guard
- * WARNING: HatsSignerGate must not be attached to a Safe with any other modules
- * WARNING: HatsSignerGate must not be attached to its Safe if `validSignerCount()` >= `_maxSigners`
- * Before wiring up HatsSignerGate to its Safe, call `canAttachHSGToSafe` and make sure the result is true
- * Failure to do so may result in the Safe being locked forever
- */
- function deployHatsSignerGate(
- uint256 _ownerHatId,
- uint256 _signersHatId,
- address _safe, // existing Gnosis Safe that the signers will join
- uint256 _minThreshold,
- uint256 _targetThreshold,
- uint256 _maxSigners
- ) public returns (address hsg) {
- // disallow attaching to a safe with existing modules
- (address[] memory modulesWith1,) = GnosisSafe(payable(_safe)).getModulesPaginated(SENTINEL_MODULES, 1);
- if (modulesWith1.length > 0) revert NoOtherModulesAllowed();
-
- return _deployHatsSignerGate(_ownerHatId, _signersHatId, _safe, _minThreshold, _targetThreshold, _maxSigners);
- }
-
- /**
- * @notice Checks if a HatsSignerGate can be safely attached to a Safe
- * @dev There must be...
- * 1) No existing modules on the Safe
- * 2) HatsSignerGate's `validSignerCount()` must be <= `_maxSigners`
- */
- function canAttachHSGToSafe(HatsSignerGate _hsg) public view returns (bool) {
- (address[] memory modulesWith1,) = _hsg.safe().getModulesPaginated(SENTINEL_MODULES, 1);
- uint256 moduleCount = modulesWith1.length;
-
- return (moduleCount == 0 && _hsg.validSignerCount() <= _hsg.maxSigners());
- }
-
- function _deployHatsSignerGate(
- uint256 _ownerHatId,
- uint256 _signersHatId,
- address _safe, // existing Gnosis Safe that the signers will join
- uint256 _minThreshold,
- uint256 _targetThreshold,
- uint256 _maxSigners
- ) internal returns (address hsg) {
- bytes memory initializeParams = abi.encode(
- _ownerHatId, _signersHatId, _safe, hatsAddress, _minThreshold, _targetThreshold, _maxSigners, version
- );
-
- hsg = moduleProxyFactory.deployModule(
- hatsSignerGateSingleton, abi.encodeWithSignature("setUp(bytes)", initializeParams), ++nonce
- );
-
- emit HatsSignerGateSetup(hsg, _ownerHatId, _signersHatId, _safe, _minThreshold, _targetThreshold, _maxSigners);
- }
-
- function _generateMultisendAction(address _hatsSignerGate, address _safe)
- internal
- pure
- returns (bytes memory _action)
- {
- bytes memory enableHSGModule = abi.encodeWithSignature("enableModule(address)", _hatsSignerGate);
-
- // Generate delegate call so the safe calls setGuard on itself during setup
- bytes memory setHSGGuard = abi.encodeWithSignature("setGuard(address)", _hatsSignerGate);
-
- bytes memory packedCalls = abi.encodePacked(
- // enableHSGModule
- uint8(0), // 0 for call; 1 for delegatecall
- _safe, // to
- uint256(0), // value
- uint256(enableHSGModule.length), // data length
- bytes(enableHSGModule), // data
- // setHSGGuard
- uint8(0), // 0 for call; 1 for delegatecall
- _safe, // to
- uint256(0), // value
- uint256(setHSGGuard.length), // data length
- bytes(setHSGGuard) // data
- );
-
- _action = abi.encodeWithSignature("multiSend(bytes)", packedCalls);
- }
-
- /// @notice Deploy a new MultiHatsSignerGate and a new Safe, all wired up together
- function deployMultiHatsSignerGateAndSafe(
- uint256 _ownerHatId,
- uint256[] calldata _signersHatIds,
- uint256 _minThreshold,
- uint256 _targetThreshold,
- uint256 _maxSigners
- ) public returns (address mhsg, address payable safe) {
- // Deploy new safe but do not set it up yet
- safe = payable(gnosisSafeProxyFactory.createProxy(safeSingleton, hex"00"));
-
- // Deploy new hats signer gate
- mhsg =
- _deployMultiHatsSignerGate(_ownerHatId, _signersHatIds, safe, _minThreshold, _targetThreshold, _maxSigners);
-
- // Generate delegate call so the safe calls enableModule on itself during setup
- bytes memory multisendAction = _generateMultisendAction(mhsg, safe);
-
- // Workaround for solidity dynamic memory array
- address[] memory owners = new address[](1);
- owners[0] = mhsg;
-
- // Call setup on safe to enable our new module/guard and set it as the sole initial owner
- GnosisSafe(safe).setup(
- owners,
- 1,
- gnosisMultisendLibrary,
- multisendAction, // set hsg as module and guard
- gnosisFallbackLibrary,
- address(0),
- 0,
- payable(address(0))
- );
-
- emit MultiHatsSignerGateSetup(
- mhsg, _ownerHatId, _signersHatIds, safe, _minThreshold, _targetThreshold, _maxSigners
- );
-
- return (mhsg, safe);
- }
-
- /**
- * @notice Deploy a new MultiHatsSignerGate and relate it to an existing Safe
- * @dev In order to wire it up to the existing Safe, the owners of the Safe must enable it as a module and guard
- * WARNING: MultiHatsSignerGate must not be attached to a Safe with any other modules
- * WARNING: MultiHatsSignerGate must not be attached to its Safe if `validSignerCount()` > `_maxSigners`
- * Before wiring up MultiHatsSignerGate to its Safe, call `canAttachMHSGToSafe` and make sure the result is true
- * Failure to do so may result in the Safe being locked forever
- */
- function deployMultiHatsSignerGate(
- uint256 _ownerHatId,
- uint256[] calldata _signersHatIds,
- address _safe, // existing Gnosis Safe that the signers will join
- uint256 _minThreshold,
- uint256 _targetThreshold,
- uint256 _maxSigners
- ) public returns (address mhsg) {
- // // disallow attaching to a safe with existing modules
- (address[] memory modulesWith1,) = GnosisSafe(payable(_safe)).getModulesPaginated(SENTINEL_MODULES, 1);
- if (modulesWith1.length > 0) revert NoOtherModulesAllowed();
-
- return
- _deployMultiHatsSignerGate(_ownerHatId, _signersHatIds, _safe, _minThreshold, _targetThreshold, _maxSigners);
- }
-
- /**
- * @notice Checks if a MultiHatsSignerGate can be safely attached to a Safe
- * @dev There must be...
- * 1) No existing modules on the Safe
- * 2) MultiHatsSignerGate's `validSignerCount()` must be <= `_maxSigners`
- */
- function canAttachMHSGToSafe(MultiHatsSignerGate _mhsg) public view returns (bool) {
- (address[] memory modulesWith1,) = _mhsg.safe().getModulesPaginated(SENTINEL_MODULES, 1);
- uint256 moduleCount = modulesWith1.length;
-
- return (moduleCount == 0 && _mhsg.validSignerCount() <= _mhsg.maxSigners());
- }
-
- function _deployMultiHatsSignerGate(
- uint256 _ownerHatId,
- uint256[] calldata _signersHatIds,
- address _safe, // existing Gnosis Safe that the signers will join
- uint256 _minThreshold,
- uint256 _targetThreshold,
- uint256 _maxSigners
- ) internal returns (address mhsg) {
- bytes memory initializeParams = abi.encode(
- _ownerHatId, _signersHatIds, _safe, hatsAddress, _minThreshold, _targetThreshold, _maxSigners, version
- );
-
- mhsg = moduleProxyFactory.deployModule(
- multiHatsSignerGateSingleton, abi.encodeWithSignature("setUp(bytes)", initializeParams), ++nonce
- );
-
- emit MultiHatsSignerGateSetup(
- mhsg, _ownerHatId, _signersHatIds, _safe, _minThreshold, _targetThreshold, _maxSigners
- );
- }
-}
diff --git a/src/Interfaces/IGnosisSafe.sol b/src/Interfaces/IGnosisSafe.sol
deleted file mode 100644
index 16df42e..0000000
--- a/src/Interfaces/IGnosisSafe.sol
+++ /dev/null
@@ -1,50 +0,0 @@
-// SPDX-License-Identifier: CC0
-pragma solidity ^0.8.4;
-
-import { Enum } from "@gnosis.pm/safe-contracts/contracts/common/Enum.sol";
-
-interface IGnosisSafe {
- function addOwnerWithThreshold(address owner, uint256 _threshold) external;
-
- function swapOwner(address prevOwner, address oldOwner, address newOwner) external;
-
- function removeOwner(address prevOwner, address owner, uint256 _threshold) external;
-
- function changeThreshold(uint256 _threshold) external;
-
- function nonce() external returns (uint256);
-
- function getThreshold() external returns (uint256);
-
- function approvedHashes(address approver, bytes32 hash) external returns (uint256);
-
- function domainSeparator() external view returns (bytes32);
-
- function getOwners() external view returns (address[] memory);
-
- function setGuard(address guard) external;
-
- function execTransactionFromModule(address to, uint256 value, bytes memory data, Enum.Operation operation)
- external
- returns (bool success);
-
- function getTransactionHash(
- address to,
- uint256 value,
- bytes calldata data,
- Enum.Operation operation,
- uint256 safeTxGas,
- uint256 baseGas,
- uint256 gasPrice,
- address gasToken,
- address refundReceiver,
- uint256 _nonce
- ) external view returns (bytes32);
-
- function isOwner(address owner) external returns (bool);
-
- function getModulesPaginated(address start, uint256 pageSize)
- external
- view
- returns (address[] memory array, address next);
-}
diff --git a/src/MultiHatsSignerGate.sol b/src/MultiHatsSignerGate.sol
deleted file mode 100644
index be652f9..0000000
--- a/src/MultiHatsSignerGate.sol
+++ /dev/null
@@ -1,116 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity >=0.8.13;
-
-// import { Test, console2 } from "forge-std/Test.sol"; // remove after testing
-import { HatsSignerGateBase, IGnosisSafe, Enum } from "./HatsSignerGateBase.sol";
-import "./HSGLib.sol";
-
-contract MultiHatsSignerGate is HatsSignerGateBase {
- /// @notice Append-only tracker of approved signer hats
- mapping(uint256 => bool) public validSignerHats;
-
- /// @notice Tracks the hat ids worn by users who have "claimed signer"
- mapping(address => uint256) public claimedSignerHats;
-
- /// @notice Initializes a new instance of MultiHatsSignerGate
- /// @dev Can only be called once
- /// @param initializeParams ABI-encoded bytes with initialization parameters
- function setUp(bytes calldata initializeParams) public payable override initializer {
- (
- uint256 _ownerHatId,
- uint256[] memory _signerHats,
- address _safe,
- address _hats,
- uint256 _minThreshold,
- uint256 _targetThreshold,
- uint256 _maxSigners,
- string memory _version
- ) = abi.decode(initializeParams, (uint256, uint256[], address, address, uint256, uint256, uint256, string));
-
- _setUp(_ownerHatId, _safe, _hats, _minThreshold, _targetThreshold, _maxSigners, _version);
-
- _addSignerHats(_signerHats);
- }
-
- /// @notice Function to become an owner on the safe if you are wearing `_hatId` and `_hatId` is a valid signer hat
- /// @dev Reverts if `maxSigners` has been reached, the caller is either invalid or has already claimed. Swaps caller with existing invalid owner if relevant.
- /// @param _hatId The hat id to claim signer rights for
- function claimSigner(uint256 _hatId) public {
- uint256 maxSigs = maxSigners; // save SLOADs
- address[] memory owners = safe.getOwners();
- uint256 currentSignerCount = _countValidSigners(owners);
-
- if (currentSignerCount >= maxSigs) {
- revert MaxSignersReached();
- }
-
- if (safe.isOwner(msg.sender)) {
- revert SignerAlreadyClaimed(msg.sender);
- }
-
- if (!isValidSignerHat(_hatId)) {
- revert InvalidSignerHat(_hatId);
- }
-
- if (!HATS.isWearerOfHat(msg.sender, _hatId)) {
- revert NotSignerHatWearer(msg.sender);
- }
-
- /*
- We check the safe owner count in case there are existing owners who are no longer valid signers.
- If we're already at maxSigners, we'll replace one of the invalid owners by swapping the signer.
- Otherwise, we'll simply add the new signer.
- */
- uint256 ownerCount = owners.length;
-
- if (ownerCount >= maxSigs) {
- bool swapped = _swapSigner(owners, ownerCount, msg.sender);
- if (!swapped) {
- // if there are no invalid owners, we can't add a new signer, so we revert
- revert NoInvalidSignersToReplace();
- }
- } else {
- _grantSigner(owners, currentSignerCount, msg.sender);
- }
-
- // register the hat used to claim. This will be the hat checked in `checkTransaction()` for this signer
- claimedSignerHats[msg.sender] = _hatId;
- }
-
- /// @notice Checks if `_account` is a valid signer, ie is wearing the signer hat
- /// @dev Must be implemented by all flavors of HatsSignerGate
- /// @param _account The address to check
- /// @return valid Whether `_account` is a valid signer
- function isValidSigner(address _account) public view override returns (bool valid) {
- /// @dev existing `claimedSignerHats` are always valid, since `validSignerHats` is append-only
- valid = HATS.isWearerOfHat(_account, claimedSignerHats[_account]);
- }
-
- /// @notice Adds new approved signer hats
- /// @param _newSignerHats Array of hat ids to add as approved signer hats
- function addSignerHats(uint256[] calldata _newSignerHats) external onlyOwner {
- _addSignerHats(_newSignerHats);
-
- emit HSGLib.SignerHatsAdded(_newSignerHats);
- }
-
- /// @notice Internal function to approve new signer hats
- /// @param _newSignerHats Array of hat ids to add as approved signer hats
- function _addSignerHats(uint256[] memory _newSignerHats) internal {
- for (uint256 i = 0; i < _newSignerHats.length;) {
- validSignerHats[_newSignerHats[i]] = true;
-
- // should not overflow with feasible array length
- unchecked {
- ++i;
- }
- }
- }
-
- /// @notice A `_hatId` is valid if it is included in the `validSignerHats` mapping
- /// @param _hatId The hat id to check
- /// @return valid Whether `_hatId` is a valid signer hat
- function isValidSignerHat(uint256 _hatId) public view returns (bool valid) {
- valid = validSignerHats[_hatId];
- }
-}
diff --git a/src/interfaces/IHatsSignerGate.sol b/src/interfaces/IHatsSignerGate.sol
new file mode 100644
index 0000000..4aa2a65
--- /dev/null
+++ b/src/interfaces/IHatsSignerGate.sol
@@ -0,0 +1,313 @@
+// SPDX-License-Identifier: LGPL-3.0
+pragma solidity >=0.8.13;
+
+import { ISafe } from "../lib/safe-interfaces/ISafe.sol";
+import { IHats } from "../../lib/hats-protocol/src/Interfaces/IHats.sol";
+
+/// @notice Interface for the HatsSignerGate contract
+interface IHatsSignerGate {
+ /*//////////////////////////////////////////////////////////////
+ DATA TYPES
+ //////////////////////////////////////////////////////////////*/
+
+ /// @notice The type of target threshold
+ /// @param ABSOLUTE An absolute number of signatures
+ /// @param PROPORTIONAL A percentage of the total number of signers, in basis points (10000 = 100%)
+ enum TargetThresholdType {
+ ABSOLUTE, // 0
+ PROPORTIONAL // 1
+
+ }
+
+ /// @notice Struct for the threshold configuration
+ /// @param thresholdType The type of target threshold, either ABSOLUTE or PROPORTIONAL
+ /// @param min The minimum threshold
+ /// @param target The target. If thresholdType is ABSOLUTE, this is an absolute number of signatures.
+ /// If thresholdType is PROPORTIONAL, this is a percentage in basis points (10000 = 100%).
+ struct ThresholdConfig {
+ TargetThresholdType thresholdType;
+ uint120 min;
+ uint120 target;
+ }
+
+ /// @notice Struct for the parameters passed to the `setUp` function
+ /// @param ownerHat The ID of the owner hat
+ /// @param signerHats The IDs of the signer hats
+ /// @param safe The address of the safe
+ /// @param thresholdConfig The threshold configuration
+ /// @param locked Whether the contract is locked
+ /// @param claimableFor Whether signer permissions can be claimed on behalf of valid hat wearers
+ /// @param implementation The address of the HatsSignerGate implementation
+ /// @param hsgGuard The address of the initial guard set on the HatsSignerGate instance
+ /// @param hsgModules The initial modules set on the HatsSignerGate instance
+ struct SetupParams {
+ uint256 ownerHat;
+ uint256[] signerHats;
+ address safe;
+ ThresholdConfig thresholdConfig;
+ bool locked;
+ bool claimableFor;
+ address implementation;
+ address hsgGuard;
+ address[] hsgModules;
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ CUSTOM ERRORS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @notice Signers are not allowed to disable the HatsSignerGate guard
+ error CannotDisableThisGuard();
+
+ /// @notice Only the wearer of the owner Hat can make changes to this contract
+ error NotOwnerHatWearer();
+
+ /// @notice Only wearers of a valid signer hat can become signers
+ error NotSignerHatWearer(address user);
+
+ /// @notice Thrown when the safe threshold is lower than the number of required valid signatures
+ error ThresholdTooLow();
+
+ /// @notice Thrown when the number of signatures from valid signers is less than the correct threshold
+ error InsufficientValidSignatures();
+
+ /// @notice Can't remove a signer if they're still wearing the signer hat
+ error StillWearsSignerHat();
+
+ /// @notice Invalid threshold configuration
+ /// @dev Thrown when:
+ /// 1. ABSOLUTE threshold type: target < min
+ /// 2. PROPORTIONAL threshold type: target > 10_000 (100%)
+ /// 3. Invalid threshold type (not ABSOLUTE or PROPORTIONAL)
+ error InvalidThresholdConfig();
+
+ /// @notice Can only claim signer with a valid signer hat
+ error InvalidSignerHat(uint256 hatId);
+
+ /// @notice Neither signers nor modules enabled on HSG can change the threshold
+ error CannotChangeThreshold();
+
+ /// @notice Neither signers nor modules enabled on HSG can change the modules
+ error CannotChangeModules();
+
+ /// @notice Neither signers nor modules enabled on HSG can change the owners
+ error CannotChangeOwners();
+
+ /// @notice Neither Safe signers nor modules enabled on HSG can make external calls to the `safe`
+ /// @dev This ensures that signers and modules cannot change any of the `safe`'s settings
+ error CannotCallSafe();
+
+ /// @notice Neither signers nor modules enabled on HSG can change the fallback handler
+ error CannotChangeFallbackHandler();
+
+ /// @notice Emmitted when attempting to reenter `checkTransaction`
+ /// @dev The Safe will catch this error and re-throw with its own error message (`GS013`)
+ error NoReentryAllowed();
+
+ /// @notice Emmitted when a call to `checkTransaction` or `checkAfterExecution` is not made from the `safe`
+ /// @dev Together with `guardEntries`, protects against arbitrary reentrancy attacks by the signers
+ error NotCalledFromSafe();
+
+ /// @notice Owner cannot change settings once the contract is locked
+ error Locked();
+
+ /// @notice Signer permissions cannot be claimed on behalf of valid hat wearers if this is not set
+ error NotClaimableFor();
+
+ /// @notice The input arrays must be the same length
+ error InvalidArrayLength();
+
+ /// @notice The delegatecall target is not enabled
+ error DelegatecallTargetNotEnabled();
+
+ /// @notice Reregistration is not allowed on behalf of an existing signer
+ error ReregistrationNotAllowed();
+
+ /*//////////////////////////////////////////////////////////////
+ EVENTS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @notice Emitted when the threshold configuration is set
+ event ThresholdConfigSet(ThresholdConfig thresholdConfig);
+
+ /// @notice Emitted when new approved signer hats are added
+ event SignerHatsAdded(uint256[] newSignerHats);
+
+ /// @notice Emitted when the owner hat is updated
+ event OwnerHatSet(uint256 ownerHat);
+
+ /// @notice Emitted when the contract is locked, preventing any further changes to settings
+ event HSGLocked();
+
+ /// @notice Emitted when the claimableFor parameter is set
+ event ClaimableForSet(bool claimableFor);
+
+ /// @notice Emitted when HSG has been detached from its avatar Safe
+ event Detached();
+
+ /// @notice Emitted when HSG has been migrated to a new HSG
+ event Migrated(address newHSG);
+
+ /// @notice Emitted when a delegatecall target is enabled
+ event DelegatecallTargetEnabled(address target, bool enabled);
+
+ /// @notice Emitted when a signer registers the hat that makes them a valid signer
+ event Registered(uint256 hatId, address signer);
+
+ /*//////////////////////////////////////////////////////////////
+ CONSTANTS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @notice The Hats Protocol contract address
+ function HATS() external view returns (IHats);
+
+ /// @notice The version of this HatsSignerGate contract
+ function version() external view returns (string memory);
+
+ /*//////////////////////////////////////////////////////////////
+ STATE VARIABLES
+ //////////////////////////////////////////////////////////////*/
+
+ /// @notice Tracks the hat ids worn by users who have "claimed signer"
+ function registeredSignerHats(address) external view returns (uint256);
+
+ /// @notice Tracks enabled delegatecall targets. Enabled targets can be delegatecalled by the `safe`
+ function enabledDelegatecallTargets(address) external view returns (bool);
+
+ /// @notice The owner hat
+ function ownerHat() external view returns (uint256);
+
+ /// @notice The `safe` to which this contract is attached
+ function safe() external view returns (ISafe);
+
+ /// @notice The threshold configuration
+ function thresholdConfig() external view returns (ThresholdConfig memory);
+
+ /// @notice The address of the HatsSignerGate implementation
+ function implementation() external view returns (address);
+
+ /// @notice Whether the contract is locked. If true, the owner cannot change any of the contract's settings.
+ function locked() external view returns (bool);
+
+ /// @notice Whether signer permissions can be claimed on behalf of valid hat wearers
+ function claimableFor() external view returns (bool);
+
+ /*//////////////////////////////////////////////////////////////
+ FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @notice Initializes a new instance of HatsSignerGate.
+ /// @dev Does NOT check if the target Safe is compatible with this HSG.
+ /// @dev Can only be called once
+ /// @param initializeParams ABI-encoded bytes with initialization parameters, as defined in
+ /// {IHatsSignerGate.SetupParams}
+ function setUp(bytes calldata initializeParams) external payable;
+
+ /// @notice Claims signer permissions for the caller. Must be a valid wearer of `_hatId`.
+ /// @dev If the `_signer` is not already an owner on the `safe`, they are added as a new owner.
+ /// @param _hatId The hat id to claim signer rights for
+ function claimSigner(uint256 _hatId) external;
+
+ /// @notice Claims signer permissions for a valid wearer of `_hatId` on behalf of `_signer`.
+ /// @dev If the `_signer` is not already an owner on the `safe`, they are added as a new owner.
+ /// @param _hatId The hat id to claim signer rights for
+ /// @param _signer The address to claim signer rights for
+ function claimSignerFor(uint256 _hatId, address _signer) external;
+
+ /// @notice Claims signer permissions for a set of valid wearers of `_hatIds` on behalf of the `_signers`
+ /// If this contract is the only owner on the `safe`, it will be swapped out for the first `_signer`. Otherwise, each
+ /// of the `_signers` will be added as a new owner.
+ /// @param _hatIds The hat ids to use for adding each of the `_signers`, indexed to `_signers`
+ /// @param _signers The addresses to add as new `safe` owners, indexed to `_hatIds`
+ function claimSignersFor(uint256[] calldata _hatIds, address[] calldata _signers) external;
+
+ /// @notice Removes an invalid signer from the `safe`, updating the threshold if appropriate
+ /// @param _signer The address to remove if not a valid signer
+ function removeSigner(address _signer) external;
+
+ /// @notice Irreversibly locks the contract, preventing any further changes to the contract's settings.
+ /// @dev Only callable by a wearer of the owner hat, and only if the contract is not locked.
+ function lock() external;
+
+ /// @notice Sets the owner hat
+ /// @dev Only callable by a wearer of the current owner hat, and only if the contract is not locked
+ /// @param _ownerHat The new owner hat
+ function setOwnerHat(uint256 _ownerHat) external;
+
+ /// @notice Adds new approved signer hats
+ /// @dev Only callable by a wearer of the owner hat, and only if the contract is not locked.
+ /// @param _newSignerHats Array of hat ids to add as approved signer hats
+ function addSignerHats(uint256[] calldata _newSignerHats) external;
+
+ /// @notice Sets a new threshold configuration
+ /// @dev Only callable by a wearer of the owner hat, and only if the contract is not locked.
+ /// @param _thresholdConfig The new threshold configuration
+ function setThresholdConfig(ThresholdConfig memory _thresholdConfig) external;
+
+ /// @notice Sets whether signer permissions can be claimed on behalf of valid hat wearers
+ /// @dev Only callable by a wearer of the owner hat, and only if the contract is not locked.
+ /// @param _claimableFor Whether signer permissions can be claimed on behalf of valid hat wearers
+ function setClaimableFor(bool _claimableFor) external;
+
+ /// @notice Detach HSG from the Safe
+ /// @dev Only callable by a wearer of the owner hat, and only if the contract is not locked.
+ function detachHSG() external;
+
+ /// @notice Migrate the Safe to a new HSG, ie detach this HSG and attach a new HSG
+ /// @dev Only callable by a wearer of the owner hat, and only if the contract is not locked.
+ /// @param _newHSG The new HatsSignerGate to attach to the Safe
+ /// @param _signerHatIds The hat ids to use for adding each of the `_signersToMigrate`, indexed to `_signersToMigrate`
+ /// @param _signersToMigrate The addresses to add as new `safe` owners, indexed to `_signerHatIds`, empty if no
+ /// signers to migrate. `_newHSG` must have claimableFor==TRUE to migrate signers.
+ function migrateToNewHSG(address _newHSG, uint256[] calldata _signerHatIds, address[] calldata _signersToMigrate)
+ external;
+
+ /// @notice Enables a target contract to be delegatecall-able by the `safe`.
+ /// @dev Only callable by a wearer of the owner hat, and only if the contract is not locked.
+ /// @param _target The target addressto enable
+ function enableDelegatecallTarget(address _target) external;
+
+ /// @notice Disables a target contract from being delegatecall-able by the `safe`.
+ /// @dev Only callable by a wearer of the owner hat, and only if the contract is not locked.
+ /// @param _target The target address to disable
+ function disableDelegatecallTarget(address _target) external;
+
+ /*//////////////////////////////////////////////////////////////
+ VIEW FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @notice Checks if `_account` is a valid signer, ie is wearing the signer hat
+ /// @dev Must be implemented by all flavors of HatsSignerGate
+ /// @param _account The address to check
+ /// @return valid Whether `_account` is a valid signer
+ function isValidSigner(address _account) external view returns (bool valid);
+
+ /// @notice A `_hatId` is valid if it is included in the `validSignerHats` mapping
+ /// @param _hatId The hat id to check
+ /// @return valid Whether `_hatId` is a valid signer hat
+ function isValidSignerHat(uint256 _hatId) external view returns (bool valid);
+
+ /// @notice Tallies the number of existing `safe` owners that wear a signer hat
+ /// @return signerCount The number of valid signers on the `safe`
+ function validSignerCount() external view returns (uint256 signerCount);
+
+ /// @notice Checks if a HatsSignerGate can be safely attached to a Safe, ie there must be no existing modules
+ /// @param _safe The Safe to check
+ /// @return canAttach Whether the HSG can be attached to the Safe
+ function canAttachToSafe(ISafe _safe) external view returns (bool canAttach);
+
+ /// @notice Returns the addresses of the Safe contracts used to deploy new Safes
+ /// @return _safeSingleton The address of the Safe singleton used to deploy new Safes
+ /// @return _safeFallbackLibrary The address of the Safe fallback library used to deploy new Safes
+ /// @return _safeMultisendLibrary The address of the Safe multisend library used to deploy new Safes
+ /// @return _safeProxyFactory The address of the Safe proxy factory used to deploy new Safes
+ function getSafeDeployParamAddresses()
+ external
+ view
+ returns (
+ address _safeSingleton,
+ address _safeFallbackLibrary,
+ address _safeMultisendLibrary,
+ address _safeProxyFactory
+ );
+}
diff --git a/src/lib/SafeManagerLib.sol b/src/lib/SafeManagerLib.sol
new file mode 100644
index 0000000..6963daf
--- /dev/null
+++ b/src/lib/SafeManagerLib.sol
@@ -0,0 +1,266 @@
+// SPDX-License-Identifier: LGPL-3.0
+pragma solidity >=0.8.13;
+
+// import { console2 } from "../lib/forge-std/src/console2.sol";
+import { MultiSend } from "../../lib/safe-smart-account/contracts/libraries/MultiSend.sol";
+import { SafeProxyFactory } from "../../lib/safe-smart-account/contracts/proxies/SafeProxyFactory.sol";
+import { StorageAccessible } from "../../lib/safe-smart-account/contracts/common/StorageAccessible.sol";
+import { Enum, ISafe, IGuardManager, IModuleManager, IOwnerManager } from "../lib/safe-interfaces/ISafe.sol";
+
+/// @title SafeManagerLib
+/// @author Haberdasher Labs
+/// @author @spengrah
+/// @notice A library for managing Safe contract settings via a HatsSignerGate module
+library SafeManagerLib {
+ /*//////////////////////////////////////////////////////////////
+ CUSTOM ERRORS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @notice Emitted when a call to the Safe's execTransactionFromModule fails
+ error SafeTransactionFailed();
+
+ /*//////////////////////////////////////////////////////////////
+ CONSTANTS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @dev The head pointer used in the Safe owners linked list, as well as the module linked list
+ address internal constant SENTINELS = address(0x1);
+
+ /// @dev The storage slot used by Safe to store the guard address: keccak256("guard_manager.guard.address")
+ bytes32 internal constant GUARD_STORAGE_SLOT = 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8;
+
+ // keccak256("fallback_manager.handler.address")
+ bytes32 internal constant FALLBACK_HANDLER_STORAGE_SLOT =
+ 0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5;
+
+ /*//////////////////////////////////////////////////////////////
+ HELPER FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @dev Deploy a new Safe and attach HSG to it
+ /// @param _safeProxyFactory The address of the SafeProxyFactory to use for deploying the Safe
+ /// @param _safeSingleton The address of the Safe singleton to use as the implementation for the Safe instance
+ /// @param _safeFallbackLibrary The address of the Safe fallback library to set on the Safe
+ /// @param _safeMultisendLibrary The address of the Safe multisend library to use to initialize the Safe and HSG
+ function deploySafeAndAttachHSG(
+ address _safeProxyFactory,
+ address _safeSingleton,
+ address _safeFallbackLibrary,
+ address _safeMultisendLibrary
+ ) internal returns (address payable _safe) {
+ _safe = payable(
+ SafeProxyFactory(_safeProxyFactory).createProxyWithNonce(_safeSingleton, hex"00", uint256(uint160(address(this))))
+ );
+
+ // Prepare calls to enable HSG as module and set it as guard
+ bytes memory enableHSGModule = encodeEnableModuleAction(address(this));
+ bytes memory setHSGGuard = encodeSetGuardAction(address(this));
+
+ bytes memory packedCalls = abi.encodePacked(
+ // enableHSGModule
+ Enum.Operation.Call, // 0 for call; 1 for delegatecall
+ _safe, // to
+ uint256(0), // value
+ uint256(enableHSGModule.length), // data length
+ bytes(enableHSGModule), // data
+ // setHSGGuard
+ Enum.Operation.Call, // 0 for call; 1 for delegatecall
+ _safe, // to
+ uint256(0), // value
+ uint256(setHSGGuard.length), // data length
+ bytes(setHSGGuard) // data
+ );
+
+ bytes memory attachHSGAction = abi.encodeWithSelector(MultiSend.multiSend.selector, packedCalls);
+
+ // Workaround for solidity dynamic memory array
+ address[] memory owners = new address[](1);
+ owners[0] = address(this);
+
+ // Call setup on safe to enable our new module/guard and set it as the sole initial owner
+ ISafe(_safe).setup(
+ owners,
+ 1,
+ _safeMultisendLibrary,
+ attachHSGAction, // set hsg as module and guard
+ _safeFallbackLibrary,
+ address(0),
+ 0,
+ payable(address(0))
+ );
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ ENCODING HELPER FUNCTIONS — MODULES
+ //////////////////////////////////////////////////////////////*/
+
+ /// @dev Encode the action to enable a module
+ function encodeEnableModuleAction(address _moduleToEnable) internal pure returns (bytes memory) {
+ return abi.encodeWithSelector(IModuleManager.enableModule.selector, _moduleToEnable);
+ }
+
+ /// @dev Encode the action to disable a module `_moduleToDisable`
+ /// @param _previousModule The previous module in the modules linked list
+ function encodeDisableModuleAction(address _previousModule, address _moduleToDisable)
+ internal
+ pure
+ returns (bytes memory)
+ {
+ return abi.encodeWithSelector(IModuleManager.disableModule.selector, _previousModule, _moduleToDisable);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ ENCODING HELPER FUNCTIONS — GUARDS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @dev Encode the action to set a `_guard`
+ function encodeSetGuardAction(address _guard) internal pure returns (bytes memory) {
+ return abi.encodeWithSelector(IGuardManager.setGuard.selector, _guard);
+ }
+
+ /// @dev Encode the action to remove HSG as a guard
+ function encodeRemoveHSGAsGuardAction() internal pure returns (bytes memory) {
+ // Generate calldata to remove HSG as a guard
+ return encodeSetGuardAction(address(0));
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ ENCODING HELPER FUNCTIONS — OWNERS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @dev Encode the action to swap the owner of a `_safe` from `_oldOwner` to `_newOwner`
+ function encodeSwapOwnerAction(address _prevOwner, address _oldOwner, address _newOwner)
+ internal
+ pure
+ returns (bytes memory)
+ {
+ return abi.encodeWithSelector(IOwnerManager.swapOwner.selector, _prevOwner, _oldOwner, _newOwner);
+ }
+
+ /// @dev Encode the action to remove an `_oldOwner` from a `_safe`, setting a `_newThreshold`
+ /// @param _prevOwner The previous owner in the owners linked list
+ function encodeRemoveOwnerAction(address _prevOwner, address _oldOwner, uint256 _newThreshold)
+ internal
+ pure
+ returns (bytes memory)
+ {
+ return abi.encodeWithSelector(IOwnerManager.removeOwner.selector, _prevOwner, _oldOwner, _newThreshold);
+ }
+
+ /// @dev Encode the action to add an `_owner` to a `_safe`, setting a `_newThreshold`
+ function encodeAddOwnerWithThresholdAction(address _owner, uint256 _newThreshold)
+ internal
+ pure
+ returns (bytes memory)
+ {
+ return abi.encodeWithSelector(IOwnerManager.addOwnerWithThreshold.selector, _owner, _newThreshold);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ ENCODING HELPER FUNCTIONS — THRESHOLD
+ //////////////////////////////////////////////////////////////*/
+
+ /// @dev Encode the action to change the threshold of a `_safe` to `_newThreshold`
+ function encodeChangeThresholdAction(uint256 _newThreshold) internal pure returns (bytes memory) {
+ return abi.encodeWithSelector(IOwnerManager.changeThreshold.selector, _newThreshold);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ EXECUTION HELPER FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @dev Execute a transaction with `_data` from the context of a `_safe`
+ function execSafeTransactionFromHSG(ISafe _safe, bytes memory _data) internal {
+ bool success =
+ _safe.execTransactionFromModule({ to: address(_safe), value: 0, data: _data, operation: Enum.Operation.Call });
+
+ if (!success) revert SafeTransactionFailed();
+ }
+
+ /// @dev Encode the action to disable HSG as a module when there are no other modules enabled on a `_safe`
+ function execDisableHSGAsOnlyModule(ISafe _safe) internal {
+ // Generate calldata to remove HSG as a module
+ bytes memory removeHSGModule = encodeDisableModuleAction(SENTINELS, address(this));
+
+ // execute the call
+ execSafeTransactionFromHSG(_safe, removeHSGModule);
+ }
+
+ /// @dev Encode the action to disable HSG as a module on a `_safe`
+ /// @param _previousModule The previous module in the modules linked list
+ function execDisableHSGAsModule(ISafe _safe, address _previousModule) internal {
+ bytes memory removeHSGModule = encodeDisableModuleAction(_previousModule, address(this));
+
+ execSafeTransactionFromHSG(_safe, removeHSGModule);
+ }
+
+ /// @dev Remove HSG as a guard on a `_safe`
+ /// @param _safe The Safe from which to remove HSG as a guard
+ function execRemoveHSGAsGuard(ISafe _safe) internal {
+ bytes memory removeHSGGuard = encodeSetGuardAction(address(0));
+
+ execSafeTransactionFromHSG(_safe, removeHSGGuard);
+ }
+
+ /// @dev Attach a new HSG `_newHSG` to a `_safe`
+ /// WARNING: This function does not check if `_newHSG` implements the IGuard interface. `_newHSG`s that do not will be
+ /// set as a module but not as a guard.
+ function execAttachNewHSG(ISafe _safe, address _newHSG) internal {
+ bytes memory attachHSGModule = encodeEnableModuleAction(_newHSG);
+ bytes memory setHSGGuard = encodeSetGuardAction(_newHSG);
+
+ execSafeTransactionFromHSG(_safe, setHSGGuard); // will fail but not revert if `_newHSG` does not implement IGuard
+ execSafeTransactionFromHSG(_safe, attachHSGModule);
+ }
+
+ /// @dev Execute the action to change the threshold of a `_safe` to `_newThreshold`
+ function execChangeThreshold(ISafe _safe, uint256 _newThreshold) internal {
+ execSafeTransactionFromHSG(_safe, encodeChangeThresholdAction(_newThreshold));
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ VIEW HELPER FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /// @dev Get the guard of a `_safe`
+ function getSafeGuard(ISafe _safe) internal view returns (address) {
+ return abi.decode(StorageAccessible(address(_safe)).getStorageAt(uint256(GUARD_STORAGE_SLOT), 1), (address));
+ }
+
+ /// @dev Get the fallback handler of a `_safe`
+ function getSafeFallbackHandler(ISafe _safe) internal view returns (address) {
+ return
+ abi.decode(StorageAccessible(address(_safe)).getStorageAt(uint256(FALLBACK_HANDLER_STORAGE_SLOT), 1), (address));
+ }
+
+ /// @dev Get the modules array of a `_safe` with pagination of 1
+ /// @return modulesWith1 The modules array of length 1
+ /// @return next A pointer to the next module in the linked list
+ function getModulesWith1(ISafe _safe) internal view returns (address[] memory modulesWith1, address next) {
+ (modulesWith1, next) = _safe.getModulesPaginated(SENTINELS, 1);
+ }
+
+ /// @notice Checks if a HatsSignerGate can be safely attached to a `_safe`
+ /// @dev There must be no existing modules on the `_safe`
+ function canAttachHSG(ISafe _safe) internal view returns (bool) {
+ (address[] memory modulesWith1,) = _safe.getModulesPaginated(SENTINELS, 1);
+
+ return (modulesWith1.length == 0);
+ }
+
+ /// @notice Internal function to find the previous owner of an `_owner` in an array of `_owners`, ie the pointer to
+ /// the owner to remove from the `safe` owners linked list
+ /// @param _owners An array of addresses
+ /// @param _owner The address after the one to find
+ /// @return prevOwner The owner previous to `_owner` in the `safe` linked list
+ function findPrevOwner(address[] memory _owners, address _owner) internal pure returns (address prevOwner) {
+ prevOwner = SENTINELS;
+
+ for (uint256 i; i < _owners.length; ++i) {
+ if (_owners[i] == _owner) {
+ if (i == 0) break;
+ prevOwner = _owners[i - 1];
+ }
+ }
+ }
+}
diff --git a/src/lib/safe-interfaces/IFallbackManager.sol b/src/lib/safe-interfaces/IFallbackManager.sol
new file mode 100644
index 0000000..680cd8a
--- /dev/null
+++ b/src/lib/safe-interfaces/IFallbackManager.sol
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: LGPL-3.0-only
+pragma solidity >=0.7.0 <0.9.0;
+
+/**
+ * @title IFallbackManager - A contract interface managing fallback calls made to this contract.
+ * @author @safe-global/safe-protocol
+ */
+interface IFallbackManager {
+ event ChangedFallbackHandler(address indexed handler);
+
+ /**
+ * @notice Set Fallback Handler to `handler` for the Safe.
+ * @dev Only fallback calls without value and with data will be forwarded.
+ * This can only be done via a Safe transaction.
+ * Cannot be set to the Safe itself.
+ * @param handler contract to handle fallback calls.
+ */
+ function setFallbackHandler(address handler) external;
+}
diff --git a/src/lib/safe-interfaces/IGuardManager.sol b/src/lib/safe-interfaces/IGuardManager.sol
new file mode 100644
index 0000000..8a0ca51
--- /dev/null
+++ b/src/lib/safe-interfaces/IGuardManager.sol
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: LGPL-3.0-only
+/* solhint-disable one-contract-per-file */
+pragma solidity >=0.7.0 <0.9.0;
+
+/**
+ * @title IGuardManager - A contract interface managing transaction guards which perform pre and post-checks on Safe
+ * transactions.
+ * @author @safe-global/safe-protocol
+ */
+interface IGuardManager {
+ event ChangedGuard(address indexed guard);
+
+ /**
+ * @dev Set a guard that checks transactions before execution
+ * This can only be done via a Safe transaction.
+ * ⚠️ IMPORTANT: Since a guard has full power to block Safe transaction execution,
+ * a broken guard can cause a denial of service for the Safe. Make sure to carefully
+ * audit the guard code and design recovery mechanisms.
+ * @notice Set Transaction Guard `guard` for the Safe. Make sure you trust the guard.
+ * @param guard The address of the guard to be used or the 0 address to disable the guard
+ */
+ function setGuard(address guard) external;
+}
diff --git a/src/lib/safe-interfaces/IModuleManager.sol b/src/lib/safe-interfaces/IModuleManager.sol
new file mode 100644
index 0000000..299c5ea
--- /dev/null
+++ b/src/lib/safe-interfaces/IModuleManager.sol
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: LGPL-3.0-only
+pragma solidity >=0.7.0 <0.9.0;
+
+import { Enum } from "../../../lib/safe-smart-account/contracts/common/Enum.sol";
+
+/**
+ * @title IModuleManager - An interface of contract managing Safe modules
+ * @notice Modules are extensions with unlimited access to a Safe that can be added to a Safe by its owners.
+ * ⚠️ WARNING: Modules are a security risk since they can execute arbitrary transactions,
+ * so only trusted and audited modules should be added to a Safe. A malicious module can
+ * completely takeover a Safe.
+ * @author @safe-global/safe-protocol
+ */
+interface IModuleManager {
+ event EnabledModule(address indexed module);
+ event DisabledModule(address indexed module);
+ event ExecutionFromModuleSuccess(address indexed module);
+ event ExecutionFromModuleFailure(address indexed module);
+ event ChangedModuleGuard(address indexed moduleGuard);
+
+ /**
+ * @notice Enables the module `module` for the Safe.
+ * @dev This can only be done via a Safe transaction.
+ * @param module Module to be whitelisted.
+ */
+ function enableModule(address module) external;
+
+ /**
+ * @notice Disables the module `module` for the Safe.
+ * @dev This can only be done via a Safe transaction.
+ * @param prevModule Previous module in the modules linked list.
+ * @param module Module to be removed.
+ */
+ function disableModule(address prevModule, address module) external;
+
+ /**
+ * @notice Execute `operation` (0: Call, 1: DelegateCall) to `to` with `value` (Native Token)
+ * @dev Function is virtual to allow overriding for L2 singleton to emit an event for indexing.
+ * @param to Destination address of module transaction.
+ * @param value Ether value of module transaction.
+ * @param data Data payload of module transaction.
+ * @param operation Operation type of module transaction.
+ * @return success Boolean flag indicating if the call succeeded.
+ */
+ function execTransactionFromModule(address to, uint256 value, bytes memory data, Enum.Operation operation)
+ external
+ returns (bool success);
+
+ /**
+ * @notice Execute `operation` (0: Call, 1: DelegateCall) to `to` with `value` (Native Token) and return data
+ * @param to Destination address of module transaction.
+ * @param value Ether value of module transaction.
+ * @param data Data payload of module transaction.
+ * @param operation Operation type of module transaction.
+ * @return success Boolean flag indicating if the call succeeded.
+ * @return returnData Data returned by the call.
+ */
+ function execTransactionFromModuleReturnData(address to, uint256 value, bytes memory data, Enum.Operation operation)
+ external
+ returns (bool success, bytes memory returnData);
+
+ /**
+ * @notice Returns if an module is enabled
+ * @return True if the module is enabled
+ */
+ function isModuleEnabled(address module) external view returns (bool);
+
+ /**
+ * @notice Returns an array of modules.
+ * If all entries fit into a single page, the next pointer will be 0x1.
+ * If another page is present, next will be the last element of the returned array.
+ * @param start Start of the page. Has to be a module or start pointer (0x1 address)
+ * @param pageSize Maximum number of modules that should be returned. Has to be > 0
+ * @return array Array of modules.
+ * @return next Start of the next page.
+ */
+ function getModulesPaginated(address start, uint256 pageSize)
+ external
+ view
+ returns (address[] memory array, address next);
+
+ /**
+ * @dev Set a module guard that checks transactions initiated by the module before execution
+ * This can only be done via a Safe transaction.
+ * ⚠️ IMPORTANT: Since a module guard has full power to block Safe transaction execution initiatied via a module,
+ * a broken module guard can cause a denial of service for the Safe modules. Make sure to carefully
+ * audit the module guard code and design recovery mechanisms.
+ * @notice Set Module Guard `moduleGuard` for the Safe. Make sure you trust the module guard.
+ * @param moduleGuard The address of the module guard to be used or the zero address to disable the module guard.
+ */
+ function setModuleGuard(address moduleGuard) external;
+}
diff --git a/src/lib/safe-interfaces/IOwnerManager.sol b/src/lib/safe-interfaces/IOwnerManager.sol
new file mode 100644
index 0000000..c5363f6
--- /dev/null
+++ b/src/lib/safe-interfaces/IOwnerManager.sol
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: LGPL-3.0-only
+pragma solidity >=0.7.0 <0.9.0;
+
+/**
+ * @title IOwnerManager - Interface for contract which manages Safe owners and a threshold to authorize transactions.
+ * @author @safe-global/safe-protocol
+ */
+interface IOwnerManager {
+ event AddedOwner(address indexed owner);
+ event RemovedOwner(address indexed owner);
+ event ChangedThreshold(uint256 threshold);
+
+ /**
+ * @notice Adds the owner `owner` to the Safe and updates the threshold to `_threshold`.
+ * @dev This can only be done via a Safe transaction.
+ * @param owner New owner address.
+ * @param _threshold New threshold.
+ */
+ function addOwnerWithThreshold(address owner, uint256 _threshold) external;
+
+ /**
+ * @notice Removes the owner `owner` from the Safe and updates the threshold to `_threshold`.
+ * @dev This can only be done via a Safe transaction.
+ * @param prevOwner Owner that pointed to the owner to be removed in the linked list
+ * @param owner Owner address to be removed.
+ * @param _threshold New threshold.
+ */
+ function removeOwner(address prevOwner, address owner, uint256 _threshold) external;
+
+ /**
+ * @notice Replaces the owner `oldOwner` in the Safe with `newOwner`.
+ * @dev This can only be done via a Safe transaction.
+ * @param prevOwner Owner that pointed to the owner to be replaced in the linked list
+ * @param oldOwner Owner address to be replaced.
+ * @param newOwner New owner address.
+ */
+ function swapOwner(address prevOwner, address oldOwner, address newOwner) external;
+
+ /**
+ * @notice Changes the threshold of the Safe to `_threshold`.
+ * @dev This can only be done via a Safe transaction.
+ * @param _threshold New threshold.
+ */
+ function changeThreshold(uint256 _threshold) external;
+
+ /**
+ * @notice Returns the number of required confirmations for a Safe transaction aka the threshold.
+ * @return Threshold number.
+ */
+ function getThreshold() external view returns (uint256);
+
+ /**
+ * @notice Returns if `owner` is an owner of the Safe.
+ * @return Boolean if owner is an owner of the Safe.
+ */
+ function isOwner(address owner) external view returns (bool);
+
+ /**
+ * @notice Returns a list of Safe owners.
+ * @return Array of Safe owners.
+ */
+ function getOwners() external view returns (address[] memory);
+}
diff --git a/src/lib/safe-interfaces/ISafe.sol b/src/lib/safe-interfaces/ISafe.sol
new file mode 100644
index 0000000..8bf1b84
--- /dev/null
+++ b/src/lib/safe-interfaces/ISafe.sol
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: LGPL-3.0-only
+pragma solidity >=0.7.0 <0.9.0;
+
+import { Enum } from "../../../lib/safe-smart-account/contracts/common/Enum.sol";
+import { IFallbackManager } from "./IFallbackManager.sol";
+import { IGuardManager } from "./IGuardManager.sol";
+import { IModuleManager } from "./IModuleManager.sol";
+import { IOwnerManager } from "./IOwnerManager.sol";
+
+/**
+ * @title ISafe - A multisignature wallet interface with support for confirmations using signed messages based on
+ * EIP-712.
+ * @author @safe-global/safe-protocol
+ */
+interface ISafe is IModuleManager, IGuardManager, IOwnerManager, IFallbackManager {
+ event SafeSetup(
+ address indexed initiator, address[] owners, uint256 threshold, address initializer, address fallbackHandler
+ );
+ event ApproveHash(bytes32 indexed approvedHash, address indexed owner);
+ event SignMsg(bytes32 indexed msgHash);
+ event ExecutionFailure(bytes32 indexed txHash, uint256 payment);
+ event ExecutionSuccess(bytes32 indexed txHash, uint256 payment);
+
+ /**
+ * @notice Sets an initial storage of the Safe contract.
+ * @dev This method can only be called once.
+ * If a proxy was created without setting up, anyone can call setup and claim the proxy.
+ * @param _owners List of Safe owners.
+ * @param _threshold Number of required confirmations for a Safe transaction.
+ * @param to Contract address for optional delegate call.
+ * @param data Data payload for optional delegate call.
+ * @param fallbackHandler Handler for fallback calls to this contract
+ * @param paymentToken Token that should be used for the payment (0 is ETH)
+ * @param payment Value that should be paid
+ * @param paymentReceiver Address that should receive the payment (or 0 if tx.origin)
+ */
+ function setup(
+ address[] calldata _owners,
+ uint256 _threshold,
+ address to,
+ bytes calldata data,
+ address fallbackHandler,
+ address paymentToken,
+ uint256 payment,
+ address payable paymentReceiver
+ ) external;
+
+ /**
+ * @notice Executes a `operation` {0: Call, 1: DelegateCall}} transaction to `to` with `value` (Native Currency)
+ * and pays `gasPrice` * `gasLimit` in `gasToken` token to `refundReceiver`.
+ * @dev The fees are always transferred, even if the user transaction fails.
+ * This method doesn't perform any sanity check of the transaction, such as:
+ * - if the contract at `to` address has code or not
+ * - if the `gasToken` is a contract or not
+ * It is the responsibility of the caller to perform such checks.
+ * @param to Destination address of Safe transaction.
+ * @param value Ether value of Safe transaction.
+ * @param data Data payload of Safe transaction.
+ * @param operation Operation type of Safe transaction.
+ * @param safeTxGas Gas that should be used for the Safe transaction.
+ * @param baseGas Gas costs that are independent of the transaction execution(e.g. base transaction fee, signature
+ * check, payment of the refund)
+ * @param gasPrice Gas price that should be used for the payment calculation.
+ * @param gasToken Token address (or 0 if ETH) that is used for the payment.
+ * @param refundReceiver Address of receiver of gas payment (or 0 if tx.origin).
+ * @param signatures Signature data that should be verified.
+ * Can be packed ECDSA signature ({bytes32 r}{bytes32 s}{uint8 v}), contract signature (EIP-1271) or
+ * approved hash.
+ * @return success Boolean indicating transaction's success.
+ */
+ function execTransaction(
+ address to,
+ uint256 value,
+ bytes calldata data,
+ Enum.Operation operation,
+ uint256 safeTxGas,
+ uint256 baseGas,
+ uint256 gasPrice,
+ address gasToken,
+ address payable refundReceiver,
+ bytes memory signatures
+ ) external payable returns (bool success);
+
+ /**
+ * @notice Checks whether the signature provided is valid for the provided data and hash. Reverts otherwise.
+ * @param dataHash Hash of the data (could be either a message hash or transaction hash)
+ * @param signatures Signature data that should be verified.
+ * Can be packed ECDSA signature ({bytes32 r}{bytes32 s}{uint8 v}), contract signature (EIP-1271) or
+ * approved hash.
+ */
+ function checkSignatures(bytes32 dataHash, bytes memory signatures) external view;
+
+ /**
+ * @notice Checks whether the signature provided is valid for the provided data and hash. Reverts otherwise.
+ * @dev Since the EIP-1271 does an external call, be mindful of reentrancy attacks.
+ * @param executor Address that executing the transaction.
+ * ⚠️⚠️⚠️ Make sure that the executor address is a legitmate executor.
+ * Incorrectly passed the executor might reduce the threshold by 1 signature. ⚠️⚠️⚠️
+ * @param dataHash Hash of the data (could be either a message hash or transaction hash)
+ * @param signatures Signature data that should be verified.
+ * Can be packed ECDSA signature ({bytes32 r}{bytes32 s}{uint8 v}), contract signature (EIP-1271) or
+ * approved hash.
+ * @param requiredSignatures Amount of required valid signatures.
+ */
+ function checkNSignatures(address executor, bytes32 dataHash, bytes memory signatures, uint256 requiredSignatures)
+ external
+ view;
+
+ /**
+ * @notice Marks hash `hashToApprove` as approved.
+ * @dev This can be used with a pre-approved hash transaction signature.
+ * IMPORTANT: The approved hash stays approved forever. There's no revocation mechanism, so it behaves similarly
+ * to ECDSA signatures
+ * @param hashToApprove The hash to mark as approved for signatures that are verified by this contract.
+ */
+ function approveHash(bytes32 hashToApprove) external;
+
+ /**
+ * @dev Returns the domain separator for this contract, as defined in the EIP-712 standard.
+ * @return bytes32 The domain separator hash.
+ */
+ function domainSeparator() external view returns (bytes32);
+
+ /**
+ * @notice Returns transaction hash to be signed by owners.
+ * @param to Destination address.
+ * @param value Ether value.
+ * @param data Data payload.
+ * @param operation Operation type.
+ * @param safeTxGas Gas that should be used for the safe transaction.
+ * @param baseGas Gas costs for data used to trigger the safe transaction.
+ * @param gasPrice Maximum gas price that should be used for this transaction.
+ * @param gasToken Token address (or 0 if ETH) that is used for the payment.
+ * @param refundReceiver Address of receiver of gas payment (or 0 if tx.origin).
+ * @param _nonce Transaction nonce.
+ * @return Transaction hash.
+ */
+ function getTransactionHash(
+ address to,
+ uint256 value,
+ bytes calldata data,
+ Enum.Operation operation,
+ uint256 safeTxGas,
+ uint256 baseGas,
+ uint256 gasPrice,
+ address gasToken,
+ address refundReceiver,
+ uint256 _nonce
+ ) external view returns (bytes32);
+
+ /**
+ * External getter function for state variables.
+ */
+
+ /**
+ * @notice Returns the version of the Safe contract.
+ * @return Version string.
+ */
+ // solhint-disable-next-line
+ function VERSION() external view returns (string memory);
+
+ /**
+ * @notice Returns the nonce of the Safe contract.
+ * @return Nonce.
+ */
+ function nonce() external view returns (uint256);
+
+ /**
+ * @notice Returns a uint if the messageHash is signed by the owner.
+ * @param messageHash Hash of message that should be checked.
+ * @return Number denoting if an owner signed the hash.
+ */
+ function signedMessages(bytes32 messageHash) external view returns (uint256);
+
+ /**
+ * @notice Returns a uint if the messageHash is approved by the owner.
+ * @param owner Owner address that should be checked.
+ * @param messageHash Hash of message that should be checked.
+ * @return Number denoting if an owner approved the hash.
+ */
+ function approvedHashes(address owner, bytes32 messageHash) external view returns (uint256);
+}
diff --git a/src/lib/zodiac-modified/GuardableUnowned.sol b/src/lib/zodiac-modified/GuardableUnowned.sol
new file mode 100644
index 0000000..81050b0
--- /dev/null
+++ b/src/lib/zodiac-modified/GuardableUnowned.sol
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: LGPL-3.0-only
+pragma solidity >=0.7.0 <0.9.0;
+
+import { BaseGuard } from "../../../lib/zodiac/contracts/guard/BaseGuard.sol";
+import { IGuard } from "../../../lib/zodiac/contracts/interfaces/IGuard.sol";
+
+/// @title Guardable - A contract that manages fallback calls made to this contract
+/// @author Gnosis Guild
+/// @dev Modified from Zodiac's Guardable to enable inheriting contracts to use their preferred owner logic.
+/// https://github.com/gnosisguild/zodiac/blob/5165ce2f377c291d4bfe71d21948d9df0fdf6224/contracts/guard/Guardable.sol
+/// Modifications:
+/// - Removed owner logic
+contract GuardableUnowned {
+ address public guard;
+
+ event ChangedGuard(address guard);
+
+ /// `guard` does not implement IERC165.
+ error NotIERC165Compliant(address guard);
+
+ /// @dev Set a guard that checks transactions before execution.
+ /// @param _guard The address of the guard to be used or the 0 address to disable the guard.
+ function _setGuard(address _guard) internal virtual {
+ if (_guard != address(0)) {
+ if (!BaseGuard(_guard).supportsInterface(type(IGuard).interfaceId)) {
+ revert NotIERC165Compliant(_guard);
+ }
+ }
+ guard = _guard;
+ emit ChangedGuard(_guard);
+ }
+
+ function getGuard() public view virtual returns (address _guard) {
+ return guard;
+ }
+}
diff --git a/src/lib/zodiac-modified/ModifierUnowned.sol b/src/lib/zodiac-modified/ModifierUnowned.sol
new file mode 100644
index 0000000..a88a13d
--- /dev/null
+++ b/src/lib/zodiac-modified/ModifierUnowned.sol
@@ -0,0 +1,169 @@
+// SPDX-License-Identifier: LGPL-3.0-only
+pragma solidity >=0.7.0 <0.9.0;
+
+import { Enum } from "../../lib/safe-interfaces/ISafe.sol";
+import { IAvatar } from "../../../lib/zodiac/contracts/interfaces/IAvatar.sol";
+
+/// @title ModifierUnowned - A contract that sits between a Module and an Avatar and enforces some additional logic.
+/// @author Gnosis Guild
+/// @dev Modified from Zodiac's Modifier to enable inheriting contracts to use their preferred owner logic
+/// and to simplify the moduleOnly modifier.
+/// https://github.com/gnosisguild/zodiac/blob/5165ce2f377c291d4bfe71d21948d9df0fdf6224/contracts/core/Modifier.sol
+/// Modifications:
+/// - Removed Module, SignatureChecker, and ExecutionTracker inheritance
+/// - Removed owner logic
+/// - Simplified moduleOnly modifier
+abstract contract ModifierUnowned is IAvatar {
+ address internal constant SENTINEL_MODULES = address(0x1);
+ /// Mapping of modules.
+ mapping(address => address) internal modules;
+
+ /// `sender` is not an authorized module.
+ /// @param sender The address of the sender.
+ error NotAuthorized(address sender);
+
+ /// `module` is invalid.
+ error InvalidModule(address module);
+
+ /// `pageSize` is invalid.
+ error InvalidPageSize();
+
+ /// `module` is already disabled.
+ error AlreadyDisabledModule(address module);
+
+ /// `module` is already enabled.
+ error AlreadyEnabledModule(address module);
+
+ /// @dev `setModules()` was already called.
+ error SetupModulesAlreadyCalled();
+
+ /*
+ --------------------------------------------------
+ You must override both of the following virtual functions,
+ execTransactionFromModule() and execTransactionFromModuleReturnData().
+ It is recommended that implementations of both functions make use the
+ onlyModule modifier.
+ */
+
+ /// @dev Passes a transaction to the modifier.
+ /// @notice Can only be called by enabled modules.
+ /// @param to Destination address of module transaction.
+ /// @param value Ether value of module transaction.
+ /// @param data Data payload of module transaction.
+ /// @param operation Operation type of module transaction.
+ function execTransactionFromModule(address to, uint256 value, bytes calldata data, Enum.Operation operation)
+ public
+ virtual
+ returns (bool success);
+
+ /// @dev Passes a transaction to the modifier, expects return data.
+ /// @notice Can only be called by enabled modules.
+ /// @param to Destination address of module transaction.
+ /// @param value Ether value of module transaction.
+ /// @param data Data payload of module transaction.
+ /// @param operation Operation type of module transaction.
+ function execTransactionFromModuleReturnData(address to, uint256 value, bytes calldata data, Enum.Operation operation)
+ public
+ virtual
+ returns (bool success, bytes memory returnData);
+
+ /*
+ --------------------------------------------------
+ */
+
+ /// @dev Simplified version of the moduleOnly modifier from Zodiac
+ modifier moduleOnly() {
+ if (modules[msg.sender] == address(0)) revert NotAuthorized(msg.sender);
+ _;
+ }
+
+ /// @notice Disables a module on the modifier.
+ /// @dev Should be overridden to restrict access, such as to an owner
+ /// @param prevModule Module that pointed to the module to be removed in the linked list.
+ /// @param module Module to be removed.
+ function disableModule(address prevModule, address module) public virtual {
+ if (module == address(0) || module == SENTINEL_MODULES) {
+ revert InvalidModule(module);
+ }
+ if (modules[prevModule] != module) revert AlreadyDisabledModule(module);
+ modules[prevModule] = modules[module];
+ modules[module] = address(0);
+ emit DisabledModule(module);
+ }
+
+ /// @notice Enables a module that can add transactions to the queue
+ /// @dev Should be overridden to restrict access, such as to an owner
+ /// @param module Address of the module to be enabled
+ function _enableModule(address module) internal virtual {
+ if (module == address(0) || module == SENTINEL_MODULES) {
+ revert InvalidModule(module);
+ }
+ if (modules[module] != address(0)) revert AlreadyEnabledModule(module);
+ modules[module] = modules[SENTINEL_MODULES];
+ modules[SENTINEL_MODULES] = module;
+ emit EnabledModule(module);
+ }
+
+ /// @dev Returns if an module is enabled
+ /// @return True if the module is enabled
+ function isModuleEnabled(address _module) public view returns (bool) {
+ return SENTINEL_MODULES != _module && modules[_module] != address(0);
+ }
+
+ /// @dev Returns array of modules.
+ /// If all entries fit into a single page, the next pointer will be 0x1.
+ /// If another page is present, next will be the last element of the returned array.
+ /// @param start Start of the page. Has to be a module or start pointer (0x1 address)
+ /// @param pageSize Maximum number of modules that should be returned. Has to be > 0
+ /// @return array Array of modules.
+ /// @return next Start of the next page.
+ function getModulesPaginated(address start, uint256 pageSize)
+ external
+ view
+ returns (address[] memory array, address next)
+ {
+ if (start != SENTINEL_MODULES && !isModuleEnabled(start)) {
+ revert InvalidModule(start);
+ }
+ if (pageSize == 0) {
+ revert InvalidPageSize();
+ }
+
+ // Init array with max page size
+ array = new address[](pageSize);
+
+ // Populate return array
+ uint256 moduleCount = 0;
+ next = modules[start];
+ while (next != address(0) && next != SENTINEL_MODULES && moduleCount < pageSize) {
+ array[moduleCount] = next;
+ next = modules[next];
+ moduleCount++;
+ }
+
+ // Because of the argument validation we can assume that
+ // the `currentModule` will always be either a module address
+ // or sentinel address (aka the end). If we haven't reached the end
+ // inside the loop, we need to set the next pointer to the last element
+ // because it skipped over to the next module which is neither included
+ // in the current page nor won't be included in the next one
+ // if you pass it as a start.
+ if (next != SENTINEL_MODULES) {
+ next = array[moduleCount - 1];
+ }
+ // Set correct size of returned array
+ // solhint-disable-next-line no-inline-assembly
+ assembly {
+ mstore(array, moduleCount)
+ }
+ }
+
+ /// @dev Initializes the modules linked list.
+ /// @notice Should be called as part of the `setUp` / initializing function and can only be called once.
+ function setupModules() internal {
+ if (modules[SENTINEL_MODULES] != address(0)) {
+ revert SetupModulesAlreadyCalled();
+ }
+ modules[SENTINEL_MODULES] = SENTINEL_MODULES;
+ }
+}
diff --git a/test/HSGFactoryTestSetup.t.sol b/test/HSGFactoryTestSetup.t.sol
deleted file mode 100644
index ef6dc74..0000000
--- a/test/HSGFactoryTestSetup.t.sol
+++ /dev/null
@@ -1,132 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-import "forge-std/Test.sol";
-import { HatsSignerGate, HatsSignerGateBase } from "../src/HatsSignerGate.sol";
-import { MultiHatsSignerGate } from "../src/MultiHatsSignerGate.sol";
-import { HatsSignerGateFactory } from "../src/HatsSignerGateFactory.sol";
-import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";
-import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol";
-import "@gnosis.pm/safe-contracts/contracts/libraries/MultiSend.sol";
-import "@gnosis.pm/zodiac/factory/ModuleProxyFactory.sol";
-
-contract HSGFactoryTestSetup is Test {
- address public gnosisFallbackLibrary = address(bytes20("fallback"));
- address public gnosisMultisendLibrary = address(new MultiSend());
-
- HatsSignerGateFactory public factory;
- GnosisSafe public singletonSafe = new GnosisSafe();
- GnosisSafeProxyFactory public safeFactory = new GnosisSafeProxyFactory();
- ModuleProxyFactory public moduleProxyFactory = new ModuleProxyFactory();
- GnosisSafe public safe;
- address FIRST_ADDRESS = address(0x1);
-
- HatsSignerGate public singletonHatsSignerGate = new HatsSignerGate();
- HatsSignerGate public hatsSignerGate;
- MultiHatsSignerGate public singletonMultiHatsSignerGate = new MultiHatsSignerGate();
- MultiHatsSignerGate public multiHatsSignerGate;
-
- address public constant HATS = address(0x4a15);
-
- bytes32 public constant GUARD_STORAGE_SLOT = 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8;
-
- uint256 public ownerHat;
- uint256 public signerHat;
- uint256 public minThreshold;
- uint256 public targetThreshold;
- uint256 public maxSigners;
- string public version;
-
- address[] initSafeOwners = new address[](1);
-
- function deploySafe(address[] memory owners, uint256 threshold) public returns (GnosisSafe) {
- // encode safe setup parameters
- bytes memory params = abi.encodeWithSignature(
- "setup(address[],uint256,address,bytes,address,address,uint256,address)",
- owners,
- threshold,
- address(0), // to
- 0x0, // data
- address(0), // fallback handler
- address(0), // payment token
- 0, // payment
- address(0) // payment receiver
- );
-
- // deploy proxy of singleton from factory
- return GnosisSafe(payable(safeFactory.createProxyWithNonce(address(singletonSafe), params, 1)));
- }
-
- function deployHSGAndSafe(
- uint256 _ownerHat,
- uint256 _signerHat,
- uint256 _minThreshold,
- uint256 _targetThreshold,
- uint256 _maxSigners
- ) public returns (HatsSignerGate _hatsSignerGate, GnosisSafe _safe) {
- address hsg;
- address safe_;
- (hsg, safe_) =
- factory.deployHatsSignerGateAndSafe(_ownerHat, _signerHat, _minThreshold, _targetThreshold, _maxSigners);
-
- _hatsSignerGate = HatsSignerGate(hsg);
- _safe = GnosisSafe(payable(safe_));
- }
-
- function deployMHSGAndSafe(
- uint256 _ownerHat,
- uint256[] memory _signerHats,
- uint256 _minThreshold,
- uint256 _targetThreshold,
- uint256 _maxSigners
- ) public returns (MultiHatsSignerGate _multiHatsSignerGate, GnosisSafe _safe) {
- address mhsg;
- address safe_;
- (mhsg, safe_) = factory.deployMultiHatsSignerGateAndSafe(
- _ownerHat, _signerHats, _minThreshold, _targetThreshold, _maxSigners
- );
-
- _multiHatsSignerGate = MultiHatsSignerGate(mhsg);
- _safe = GnosisSafe(payable(safe_));
- }
-
- // borrowed from Orca (https://github.com/orcaprotocol/contracts/blob/main/contracts/utils/SafeTxHelper.sol)
- function getSafeTxHash(address to, bytes memory data, GnosisSafe _safe) public view returns (bytes32 txHash) {
- return _safe.getTransactionHash(
- to,
- 0,
- data,
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- safe.nonce()
- );
- }
-
- // modified from Orca (https://github.com/orcaprotocol/contracts/blob/main/contracts/utils/SafeTxHelper.sol)
- function executeSafeTxFrom(address from, bytes memory data, GnosisSafe _safe) public {
- safe.execTransaction(
- address(_safe),
- 0,
- data,
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- // (r,s,v) [r - from] [s - unused] [v - 1 flag for onchain approval]
- abi.encode(from, bytes32(0), bytes1(0x01))
- );
- }
-
- function mockIsWearerCall(address wearer, uint256 hat, bool result) public {
- bytes memory data = abi.encodeWithSignature("isWearerOfHat(address,uint256)", wearer, hat);
- vm.mockCall(HATS, data, abi.encode(result));
- }
-}
diff --git a/test/HSGTestSetup.t.sol b/test/HSGTestSetup.t.sol
deleted file mode 100644
index d62ecf4..0000000
--- a/test/HSGTestSetup.t.sol
+++ /dev/null
@@ -1,207 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-import "./HSGFactoryTestSetup.t.sol";
-import "./HatsSignerGateFactory.t.sol";
-import "../src/HSGLib.sol";
-import "@gnosis.pm/safe-contracts/contracts/common/SignatureDecoder.sol";
-import "@gnosis.pm/safe-contracts/contracts/common/Enum.sol";
-
-contract HSGTestSetup is HSGFactoryTestSetup, SignatureDecoder {
- address public constant SENTINELS = address(0x1);
-
- uint256[] public pks;
- address[] public addresses;
-
- mapping(address => bytes) public walletSigs;
-
- //// SETUP FUNCTION ////
-
- function setUp() public virtual {
- // set up variables
- ownerHat = uint256(1);
- signerHat = uint256(2);
- minThreshold = 2;
- targetThreshold = 2;
- maxSigners = 5;
-
- // initSafeOwners[0] = address(this);
-
- (pks, addresses) = createAddressesFromPks(10);
-
- version = "1.0";
-
- factory = new HatsSignerGateFactory(
- address(singletonHatsSignerGate),
- address(singletonMultiHatsSignerGate),
- HATS,
- address(singletonSafe),
- gnosisFallbackLibrary,
- gnosisMultisendLibrary,
- address(safeFactory),
- address(moduleProxyFactory),
- version
- );
-
- (hatsSignerGate, safe) = deployHSGAndSafe(ownerHat, signerHat, minThreshold, targetThreshold, maxSigners);
- mockIsWearerCall(address(hatsSignerGate), signerHat, false);
- }
-
- //// HELPER FUNCTIONS ////
-
- function addSigners(uint256 signerCount) public {
- for (uint256 i = 0; i < signerCount; ++i) {
- // mock mint the signerHat
- mockIsWearerCall(addresses[i], signerHat, true);
-
- // add as signer
- vm.prank(addresses[i]);
- hatsSignerGate.claimSigner();
- }
- }
-
- function getEthTransferSafeTxHash(address to, uint256 value, GnosisSafe _safe)
- public
- view
- returns (bytes32 txHash)
- {
- return _safe.getTransactionHash(
- to,
- value,
- hex"00",
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- safe.nonce()
- );
- }
-
- function getTxHash(address to, uint256 value, bytes memory data, GnosisSafe _safe)
- public
- view
- returns (bytes32 txHash)
- {
- return _safe.getTransactionHash(
- to,
- value,
- data,
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- safe.nonce()
- );
- }
-
- function createNSigsForTx(bytes32 txHash, uint256 signerCount) public returns (bytes memory signatures) {
- uint8 v;
- bytes32 r;
- bytes32 s;
- address signer;
- uint256[] memory signers = new uint256[](signerCount);
-
- for (uint256 i = 0; i < signerCount; ++i) {
- // sign txHash
- (v, r, s) = vm.sign(pks[i], txHash);
-
- signer = ecrecover(txHash, v, r, s);
-
- walletSigs[signer] = bytes.concat(r, s, bytes1(v));
- signers[i] = uint256(uint160(signer));
- }
- sort(signers, 0, int256(signerCount - 1));
-
- for (uint256 i = 0; i < signerCount; ++i) {
- address addy = address(uint160(signers[i]));
- // emit log_address(addy);
- signatures = bytes.concat(signatures, walletSigs[addy]);
- }
- }
-
- function signaturesForEthTransferTx(address to, uint256 value, uint256 signerCount, GnosisSafe _safe)
- public
- returns (bytes memory signatures)
- {
- // create tx to send some eth from safe to wherever
- bytes32 txHash = getEthTransferSafeTxHash(to, value, _safe);
- // have each signer sign the tx
- // bytes[] memory sigs = new bytes[](signerCount);
- uint8 v;
- bytes32 r;
- bytes32 s;
- address signer;
- uint256[] memory signers = new uint256[](signerCount);
-
- for (uint256 i = 0; i < signerCount; ++i) {
- // sign txHash
- (v, r, s) = vm.sign(pks[i], txHash);
-
- signer = ecrecover(txHash, v, r, s);
-
- walletSigs[signer] = bytes.concat(r, s, bytes1(v));
- signers[i] = uint256(uint160(signer));
- }
- sort(signers, 0, int256(signerCount - 1));
-
- for (uint256 i = 0; i < signerCount; ++i) {
- address addy = address(uint160(signers[i]));
- // emit log_address(addy);
- signatures = bytes.concat(signatures, walletSigs[addy]);
- }
- }
-
- function createAddressesFromPks(uint256 count)
- public
- pure
- returns (uint256[] memory pks_, address[] memory addresses_)
- {
- pks_ = new uint256[](count);
- addresses_ = new address[](count);
-
- for (uint256 i = 0; i < count; ++i) {
- pks_[i] = 100 * (i + 1);
- addresses_[i] = vm.addr(pks_[i]);
- }
- }
-
- // borrowed from https://gist.github.com/subhodi/b3b86cc13ad2636420963e692a4d896f
- function sort(uint256[] memory arr, int256 left, int256 right) internal {
- int256 i = left;
- int256 j = right;
- if (i == j) return;
- uint256 pivot = arr[uint256(left + (right - left) / 2)];
- while (i <= j) {
- while (arr[uint256(i)] < pivot) i++;
- while (pivot < arr[uint256(j)]) j--;
- if (i <= j) {
- (arr[uint256(i)], arr[uint256(j)]) = (arr[uint256(j)], arr[uint256(i)]);
- i++;
- j--;
- }
- }
- if (left < j) sort(arr, left, j);
- if (i < right) sort(arr, i, right);
- }
-
- function findPrevOwner(address[] memory _owners, address _owner) internal pure returns (address prevOwner) {
- prevOwner = SENTINELS;
-
- for (uint256 i; i < _owners.length;) {
- if (_owners[i] == _owner) {
- if (i == 0) break;
- prevOwner = _owners[i - 1];
- }
- // shouldn't overflow given reasonable _owners array length
- unchecked {
- ++i;
- }
- }
- }
-}
diff --git a/test/HatsSignerGate.attacks.t.sol b/test/HatsSignerGate.attacks.t.sol
new file mode 100644
index 0000000..3646831
--- /dev/null
+++ b/test/HatsSignerGate.attacks.t.sol
@@ -0,0 +1,1107 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.13;
+
+import { Test, console2 } from "../lib/forge-std/src/Test.sol";
+import { WithHSGInstanceTest, Enum } from "./TestSuite.t.sol";
+import { IHatsSignerGate } from "../src/interfaces/IHatsSignerGate.sol";
+import { SafeManagerLib } from "../src/lib/SafeManagerLib.sol";
+
+contract AttacksScenarios is WithHSGInstanceTest {
+ address public maliciousFallbackHandler = makeAddr("maliciousFallbackHandler");
+ address public goodFallbackHandler;
+ bytes public setFallbackAction = abi.encodeWithSignature("setFallbackHandler(address)", maliciousFallbackHandler);
+ bytes public packedCalls;
+ bytes public multiSendData;
+ bytes32 public safeTxHash;
+ bytes public checkTransactionAction;
+ bytes public signatures;
+
+ bytes public checkAfterExecutionAction =
+ abi.encodeWithSignature("checkAfterExecution(bytes32,bool)", bytes32(0), false);
+
+ string public constant CHECK_TRANSACTION_SIGNATURE =
+ "checkTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes,address)";
+
+ function setUp() public override {
+ super.setUp();
+
+ goodFallbackHandler = SafeManagerLib.getSafeFallbackHandler(safe);
+ }
+
+ function testSignersCannotAddNewModules() public {
+ _addSignersSameHat(2, signerHat);
+
+ bytes memory addModuleData = abi.encodeWithSignature("enableModule(address)", address(tstModule1));
+
+ (bytes memory multisendCall, bytes32 txHash) = _constructSingleActionMultiSendTx(addModuleData);
+
+ signatures = _createNSigsForTx(txHash, 2);
+
+ // execute tx, expecting a revert
+ vm.expectRevert(IHatsSignerGate.CannotChangeModules.selector);
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multisendCall,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ }
+
+ function testTargetSigAttackFails() public {
+ // set target threshold to 5
+ IHatsSignerGate.ThresholdConfig memory newConfig = IHatsSignerGate.ThresholdConfig({
+ thresholdType: IHatsSignerGate.TargetThresholdType.ABSOLUTE,
+ min: 2,
+ target: 5
+ });
+ vm.prank(owner);
+ instance.setThresholdConfig(newConfig);
+ // initially there are 5 signers
+ _addSignersSameHat(5, signerHat);
+
+ // 3 owners lose their hats
+ _setSignerValidity(signerAddresses[2], signerHat, false);
+ _setSignerValidity(signerAddresses[3], signerHat, false);
+ _setSignerValidity(signerAddresses[4], signerHat, false);
+
+ // the 3 owners regain their hats
+ _setSignerValidity(signerAddresses[2], signerHat, true);
+ _setSignerValidity(signerAddresses[3], signerHat, true);
+ _setSignerValidity(signerAddresses[4], signerHat, true);
+
+ // set up test values
+ // uint256 preNonce = safe.nonce();
+ uint256 preValue = 1 ether;
+ uint256 transferValue = 0.2 ether;
+ // uint256 postValue = preValue - transferValue;
+ address destAddress = signerAddresses[3];
+ // give the safe some eth
+ hoax(address(safe), preValue);
+
+ // have just 2 of 5 signers sign it
+ // create the tx
+ bytes32 txHash = _getTxHash(destAddress, transferValue, Enum.Operation.Call, hex"00", safe);
+ // have them sign it
+ signatures = _createNSigsForTx(txHash, 2);
+
+ vm.expectRevert();
+ safe.execTransaction(
+ destAddress,
+ transferValue,
+ hex"00",
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ }
+
+ function testRemoveSignerCorrectlyUpdates() public {
+ // sanity check that the min threshold is 2
+ assertEq(instance.thresholdConfig().min, 2);
+
+ // start with 5 valid signers
+ _addSignersSameHat(5, signerHat);
+
+ // the last two lose their hats
+ _setSignerValidity(signerAddresses[3], signerHat, false);
+ _setSignerValidity(signerAddresses[4], signerHat, false);
+
+ // the 4th regains its hat
+ _setSignerValidity(signerAddresses[3], signerHat, true);
+
+ // remove the 5th signer
+ instance.removeSigner(signerAddresses[4]);
+
+ // signer count should be 4 and threshold at target
+ assertEq(instance.validSignerCount(), 4, "valid signer count");
+ assertEq(safe.getThreshold(), instance.thresholdConfig().target, "ending threshold");
+ }
+
+ function testCanClaimToReplaceInvalidSignerAtMaxSigner() public {
+ // start with 5 valid signers (the max)
+ _addSignersSameHat(5, signerHat);
+
+ // the last one loses their hat
+ _setSignerValidity(signerAddresses[4], signerHat, false);
+
+ // a new signer valid tries to claim, and can
+ _setSignerValidity(signerAddresses[5], signerHat, true);
+ vm.prank(signerAddresses[5]);
+ instance.claimSigner(signerHat);
+ assertEq(instance.validSignerCount(), 5, "valid signer count");
+ }
+
+ function testSetTargetThresholdUpdatesThresholdCorrectly() public {
+ // set target threshold to 5
+ vm.prank(owner);
+ instance.setThresholdConfig(
+ IHatsSignerGate.ThresholdConfig({ thresholdType: IHatsSignerGate.TargetThresholdType.ABSOLUTE, min: 2, target: 5 })
+ );
+ // add 5 valid signers
+ _addSignersSameHat(5, signerHat);
+ // one loses their hat
+ _setSignerValidity(signerAddresses[4], signerHat, false);
+ // lower target threshold to 4
+ vm.prank(owner);
+ instance.setThresholdConfig(
+ IHatsSignerGate.ThresholdConfig({ thresholdType: IHatsSignerGate.TargetThresholdType.ABSOLUTE, min: 2, target: 4 })
+ );
+ // since instance.validSignerCount() is also 4, the threshold should also be 4
+ assertEq(safe.getThreshold(), 4, "threshold");
+ }
+
+ function testSetTargetThresholdCannotSetBelowMinThreshold() public {
+ assertEq(instance.thresholdConfig().min, 2, "min threshold");
+ assertEq(instance.thresholdConfig().target, 2, "target threshold");
+
+ // set target threshold to 1 — should fail
+ vm.prank(owner);
+ vm.expectRevert(IHatsSignerGate.InvalidThresholdConfig.selector);
+ instance.setThresholdConfig(
+ IHatsSignerGate.ThresholdConfig({ thresholdType: IHatsSignerGate.TargetThresholdType.ABSOLUTE, min: 2, target: 1 })
+ );
+ }
+
+ function testAttackerCannotExploitSigHandlingDifferences() public {
+ // start with 4 valid signers
+ _addSignersSameHat(4, signerHat);
+ // set target threshold (and therefore actual threshold) to 3
+ vm.prank(owner);
+ instance.setThresholdConfig(
+ IHatsSignerGate.ThresholdConfig({ thresholdType: IHatsSignerGate.TargetThresholdType.ABSOLUTE, min: 2, target: 3 })
+ );
+ assertEq(safe.getThreshold(), 3, "initial threshold");
+ assertEq(safe.nonce(), 0, "pre nonce");
+ // invalidate the 3rd signer, who will be our attacker
+ address attacker = signerAddresses[2];
+ _setSignerValidity(attacker, signerHat, false);
+
+ // Attacker crafts a tx to submit to the safe.
+ address maliciousContract = makeAddr("maliciousContract");
+ bytes memory maliciousTx = abi.encodeWithSignature("maliciousCall(uint256)", 1 ether);
+ // Attacker gets 2 of the valid signers to sign it, and adds their own (invalid) signature: NSigs = 3
+ bytes32 txHash = safe.getTransactionHash(
+ maliciousContract, // to
+ 0, // value
+ maliciousTx, // data
+ Enum.Operation.Call, // operation
+ 0, // safeTxGas
+ 0, // baseGas
+ 0, // gasPrice
+ address(0), // gasToken
+ address(0), // refundReceiver
+ safe.nonce() // nonce
+ );
+ signatures = _createNSigsForTx(txHash, 3);
+
+ // attacker adds a contract signature from the 4th signer from a previous tx
+ // since HSG doesn't check that the correct data was signed, it would be considered a valid signature
+ bytes memory contractSig = abi.encode(signerAddresses[3], bytes32(0), bytes1(0x01));
+ signatures = bytes.concat(signatures, contractSig);
+
+ // mock the maliciousTx so it would succeed if it were to be executed
+ vm.mockCall(maliciousContract, maliciousTx, abi.encode(true));
+ // attacker submits the tx to the safe, but it should fail
+ vm.expectRevert(IHatsSignerGate.InsufficientValidSignatures.selector);
+ vm.prank(attacker);
+ safe.execTransaction(
+ maliciousContract,
+ 0,
+ maliciousTx,
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ // (r,s,v) [r - from] [s - unused] [v - 1 flag for onchain approval]
+ signatures
+ );
+
+ assertEq(safe.getThreshold(), 3, "post threshold");
+ assertEq(instance.validSignerCount(), 3, "valid signer count");
+ assertEq(safe.nonce(), 0, "post nonce hasn't changed");
+ }
+
+ function test_revert_reenterCheckTransaction() public {
+ address newOwner = makeAddr("newOwner");
+ bytes memory addOwnerAction;
+ bytes memory checkTxAction;
+ // start with 3 valid signers
+ _addSignersSameHat(3, signerHat);
+ // attacker is the first of these signers
+ address attacker = signerAddresses[0];
+ assertEq(safe.getThreshold(), 2, "initial threshold");
+ assertEq(safe.getOwners().length, 3, "initial owner count");
+
+ /* attacker crafts a multisend tx to submit to the safe, with the following actions:
+ 1) add a new owner
+ — when `HSG.checkTransaction` is called, the hash of the original owner array will be stored
+ 2) directly call `HSG.checkTransaction`
+ — this will cause the hash of the new owner array (with the new owner from #1) to be stored
+ — when `HSG.checkAfterExecution` is called, the owner array check will pass even though
+ */
+
+ // 1) craft the addOwner action
+ // mock the new owner as a valid signer
+ _setSignerValidity(newOwner, signerHat, true);
+ {
+ // use scope to avoid stack too deep error
+ // compile the action
+ addOwnerAction = abi.encodeWithSignature("addOwnerWithThreshold(address,uint256)", newOwner, 2);
+
+ // 2) craft the direct checkTransaction action
+ // first craft a dummy/empty tx to pass to checkTransaction
+ bytes32 dummyTxHash = safe.getTransactionHash(
+ attacker, // send 0 eth to the attacker
+ 0,
+ hex"00",
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ safe.nonce()
+ );
+
+ // then have it signed by the attacker and a collaborator
+ // sigs =
+
+ checkTxAction = abi.encodeWithSelector(
+ instance.checkTransaction.selector,
+ // checkTransaction params
+ attacker,
+ 0,
+ hex"00",
+ Enum.Operation.Call,
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ _createNSigsForTx(dummyTxHash, 2),
+ attacker // msgSender
+ );
+
+ // now bundle the two actions into a multisend tx
+ packedCalls = abi.encodePacked(
+ // 1) add owner
+ uint8(0), // 0 for call; 1 for delegatecall
+ safe, // to
+ uint256(0), // value
+ uint256(addOwnerAction.length), // data length
+ bytes(addOwnerAction), // data
+ // 2) direct call to checkTransaction
+ uint8(0), // 0 for call; 1 for delegatecall
+ instance, // to
+ uint256(0), // value
+ uint256(checkTxAction.length), // data length
+ bytes(checkTxAction) // data
+ );
+ multiSendData = abi.encodeWithSignature("multiSend(bytes)", packedCalls);
+ }
+
+ // now get the safe tx hash and have attacker sign it with a collaborator
+ safeTxHash = safe.getTransactionHash(
+ defaultDelegatecallTargets[0], // to an approved delegatecall target
+ 0, // value
+ multiSendData, // data
+ Enum.Operation.DelegateCall, // operation
+ 0, // safeTxGas
+ 0, // baseGas
+ 0, // gasPrice
+ address(0), // gasToken
+ address(0), // refundReceiver
+ safe.nonce() // nonce
+ );
+ signatures = _createNSigsForTx(safeTxHash, 2);
+
+ // now submit the tx to the safe
+ vm.prank(attacker);
+ /*
+ Expect revert because of re-entry into checkTransaction
+ While instance will throw the NoReentryAllowed error,
+ since the error occurs within the context of the safe transaction,
+ the safe will catch the error and re-throw with its own error,
+ ie `GS013` ("Safe transaction failed when gasPrice and safeTxGas were 0")
+ */
+ vm.expectRevert(bytes("GS013"));
+ safe.execTransaction(
+ defaultDelegatecallTargets[0], // to an approved delegatecall target
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+
+ // no new owners have been added, despite the attacker's best efforts
+ assertEq(safe.getOwners().length, 3, "post owner count");
+ }
+
+ function test_revert_callCheckTransactionFromMultisend() public {
+ // our scenario starts with HSG attached to a safe, with 3 valid signers and a threshold of 2
+ _addSignersSameHat(3, signerHat);
+
+ // the attacker crafts a multisend tx that contains two actions:
+ // 1) set the maliciousFallback as the fallback
+ // 2) calls Safe.execTransaction with valid signatures. This can be an empty tx; its just there to enter the
+ // guard functions to overwrite the snapshot so the outer call can pass the checks.
+
+ // 1) set the maliciousFallbackHandler as the fallback
+ // bytes memory setFallbackAction = abi.encodeWithSignature("setFallbackHandler(address)",
+ // maliciousFallbackHandler);
+
+ // 2) call Safe.execTransaction with valid signatures
+ // get the hash of the empty action
+ bytes32 emptyTransactionHash = safe.getTransactionHash(
+ address(signerAddresses[0]), // must be non-safe target
+ 0,
+ hex"",
+ Enum.Operation.Call,
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ safe.nonce() + 1 // nonce increments after the outer call to execTransaction
+ );
+ // sufficient signers sign it
+ // bytes memory emptySigs = _createNSigsForTx(emptyTransactionHash, 2);
+
+ // get the calldata
+ bytes memory emptyExecTransactionAction = abi.encodeWithSignature(
+ "execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)",
+ address(signerAddresses[0]), // must be non-safe target
+ 0,
+ hex"",
+ Enum.Operation.Call,
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ _createNSigsForTx(emptyTransactionHash, 2)
+ );
+
+ // bundle the two actions into a multisend
+ packedCalls = abi.encodePacked(
+ // 1) setFallback
+ uint8(0), // 0 for call; 1 for delegatecall
+ safe, // to
+ uint256(0), // value
+ uint256(setFallbackAction.length), // data length
+ bytes(setFallbackAction), // data
+ // 2) execTransaction
+ uint8(0), // 0 for call; 1 for delegatecall
+ safe, // to
+ uint256(0), // value
+ uint256(emptyExecTransactionAction.length), // data length
+ bytes(emptyExecTransactionAction) // INNER action
+ );
+
+ // OUTER action
+ multiSendData = abi.encodeWithSignature("multiSend(bytes)", packedCalls);
+
+ // get the safe tx hash and have the signers sign it
+ safeTxHash = safe.getTransactionHash(
+ defaultDelegatecallTargets[0], // to an approved delegatecall target
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ safe.nonce()
+ );
+
+ signatures = _createNSigsForTx(safeTxHash, 2);
+
+ // submit the tx to the safe, expecting a revert
+ vm.prank(signerAddresses[0]);
+ /*
+ Expect revert because of re-entry into checkTransaction
+ While instance will throw the NoReentryAllowed error,
+ since the error occurs within the context of the safe transaction,
+ the safe will catch the error and re-throw with its own error,
+ ie `GS013` ("Safe transaction failed when gasPrice and safeTxGas were 0")
+ */
+ vm.expectRevert("GS013");
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+
+ // the fallback should not be different
+ address fallbackHandler = SafeManagerLib.getSafeFallbackHandler(safe);
+ assertEq(fallbackHandler, goodFallbackHandler, "fallbackHandler should be the same as before");
+ }
+
+ function test_revert_callCheckAfterExecutionInsideMultisend() public {
+ // start with 3 valid signers
+ _addSignersSameHat(3, signerHat);
+
+ // the attacker crafts a multisend tx that contains three actions:
+ // 1) set the maliciousFallbackHandler as the fallback — this will set the _inExecTransaction flag to true
+ // 2) HSG.checkAfterExecution — this will reset the _inExecTransaction flag to false
+ // 3) HSG.checkTransaction — this will set the _inExecTransaction flag to back to true and overwrite the snapshot
+
+ // 1) set the maliciousFallbackHandler as the fallback
+ // bytes memory setFallbackAction = abi.encodeWithSignature("setFallbackHandler(address)",
+ // maliciousFallbackHandler);
+
+ // 2) HSG.checkAfterExecution
+ // bytes memory checkAfterExecutionAction =
+ // abi.encodeWithSignature("checkAfterExecution(bytes32,bool)", bytes32(0), false);
+
+ // 3) HSG.checkTransaction
+ {
+ checkTransactionAction = abi.encodeWithSignature(
+ CHECK_TRANSACTION_SIGNATURE,
+ address(0),
+ 0,
+ hex"",
+ Enum.Operation.Call,
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ _createNContractSigs(2), // attacker's spoofed signatures
+ address(safe)
+ );
+
+ // bundle the three actions into a multisend
+ packedCalls = abi.encodePacked(
+ // 1) setFallback
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(safe), // to
+ uint256(0), // value
+ uint256(setFallbackAction.length), // data length
+ bytes(setFallbackAction), // data
+ // 2) checkAfterExecution
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(instance), // to
+ uint256(0), // value
+ uint256(checkAfterExecutionAction.length), // data length
+ bytes(checkAfterExecutionAction) // data
+ );
+
+ // workaround to avoid stack too deep error
+ packedCalls = abi.encodePacked(
+ packedCalls,
+ // 3) checkTransaction
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(instance), // to
+ uint256(0), // value
+ uint256(checkTransactionAction.length), // data length
+ bytes(checkTransactionAction) // data
+ );
+
+ multiSendData = abi.encodeWithSignature("multiSend(bytes)", packedCalls);
+
+ safeTxHash = _getSafeDelegatecallHash(defaultDelegatecallTargets[0], multiSendData, safe);
+
+ signatures = _createNSigsForTx(safeTxHash, 2);
+ }
+
+ // submit the tx to the safe, expecting a revert
+ vm.expectRevert("GS013");
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+
+ // the fallback should not be different
+ assertEq(
+ SafeManagerLib.getSafeFallbackHandler(safe), goodFallbackHandler, "fallbackHandler should be the same as before"
+ );
+ }
+
+ function test_revert_callCheckAfterExecutionFromMultisend() public {
+ // our scenario starts with HSG attached to a safe, with 3 valid signers and a threshold of 2
+ _addSignersSameHat(3, signerHat);
+
+ // the attacker crafts a multisend tx that contains two actions:
+ // 1) HSG.checkAfterExecution — this will reset the _inSafeExecTransaction flag to false after it was set to
+ // true in checkTransaction
+ // 2) set the maliciousFallbackHandler as the fallback
+ // 3) Safe.execTransaction
+
+ // 1) HSG.checkAfterExecution
+ // bytes memory checkAfterExecutionAction =
+ // abi.encodeWithSignature("checkAfterExecution(bytes32,bool)", bytes32(0), false);
+
+ // 2) set the maliciousFallbackHandler as the fallback
+ // bytes memory setFallbackAction = abi.encodeWithSignature("setFallbackHandler(address)",
+ // maliciousFallbackHandler);
+
+ // 3) call Safe.execTransaction with valid signatures
+ // get the hash of the empty action
+ bytes32 emptyTransactionHash = safe.getTransactionHash(
+ address(signerAddresses[0]), // must be non-safe target
+ 0,
+ hex"",
+ Enum.Operation.Call,
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ safe.nonce() + 1 // nonce increments after the outer call to execTransaction
+ );
+ // sufficient signers sign it
+ bytes memory emptySigs = _createNSigsForTx(emptyTransactionHash, 2);
+
+ // get the calldata
+ bytes memory emptyExecTransactionAction = abi.encodeWithSignature(
+ "execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)",
+ address(signerAddresses[0]), // must be non-safe target
+ 0,
+ hex"",
+ Enum.Operation.Call,
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ emptySigs
+ );
+
+ // bundle the three actions into a multisend
+ packedCalls = abi.encodePacked(
+ // checkTransaction
+ // 1) checkAfterExecution
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(instance), // to
+ uint256(0), // value
+ uint256(checkAfterExecutionAction.length), // data length
+ bytes(checkAfterExecutionAction), // data
+ // 2) setFallback
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(safe), // to
+ uint256(0), // value
+ uint256(setFallbackAction.length), // data length
+ bytes(setFallbackAction) // data
+ );
+
+ // workaround to avoid stack too deep error
+ packedCalls = abi.encodePacked(
+ packedCalls,
+ // 3) execTransaction
+ // checkTransaction
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(safe), // to
+ uint256(0), // value
+ uint256(emptyExecTransactionAction.length), // data length
+ bytes(emptyExecTransactionAction) // data
+ // checkAfterExecution
+ // checkAfterExecution
+ );
+
+ multiSendData = abi.encodeWithSignature("multiSend(bytes)", packedCalls);
+
+ // get the safe tx hash and have the signers sign it
+ safeTxHash = safe.getTransactionHash(
+ defaultDelegatecallTargets[0], // to an approved delegatecall target
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ safe.nonce()
+ );
+
+ signatures = _createNSigsForTx(safeTxHash, 2);
+
+ // submit the tx to the safe, expecting a revert
+ vm.prank(signerAddresses[0]);
+ vm.expectRevert(IHatsSignerGate.NoReentryAllowed.selector);
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+
+ // the fallback should be different
+ assertEq(
+ SafeManagerLib.getSafeFallbackHandler(safe), goodFallbackHandler, "fallbackHandler should be the same as before"
+ );
+ }
+
+ function test_revert_bypassHSGGuardByDisablingHSG() public {
+ // start with 3 valid signers
+ _addSignersSameHat(3, signerHat);
+
+ // the attacker crafts a multisend tx that contains three actions:
+ // 1) HSG.checkAfterExecution — this will reset the _inSafeExecTransaction flag to false after it was set to
+ // true in the outer call
+ // 2) Safe.setFallbackHandler to set the maliciousFallbackHandler as the fallback
+ // 3) HSG.checkTransaction — this will set the _inSafeExecTransaction flag to back to true and overwrite the
+ // snapshot
+
+ // 1) HSG.checkAfterExecution
+ // bytes memory checkAfterExecutionAction =
+ // abi.encodeWithSignature("checkAfterExecution(bytes32,bool)", bytes32(0), false);
+
+ // 2) Safe.setFallbackHandler to set the maliciousFallbackHandler as the fallback
+ // bytes memory setFallbackAction = abi.encodeWithSignature("setFallbackHandler(address)",
+ // maliciousFallbackHandler);
+
+ // 3) HSG.checkTransaction
+ checkTransactionAction = abi.encodeWithSignature(
+ CHECK_TRANSACTION_SIGNATURE,
+ address(0),
+ 0,
+ hex"",
+ Enum.Operation.Call,
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ _createNContractSigs(2), // attacker's spoofed signatures
+ address(safe)
+ );
+
+ // bundle the three actions into a multisend
+ packedCalls = abi.encodePacked(
+ // 1) checkAfterExecution
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(instance), // to
+ uint256(0), // value
+ uint256(checkAfterExecutionAction.length), // data length
+ bytes(checkAfterExecutionAction), // data
+ // 2) setFallback
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(safe), // to
+ uint256(0), // value
+ uint256(setFallbackAction.length), // data length
+ bytes(setFallbackAction) // data
+ );
+
+ // workaround to avoid stack too deep error
+ packedCalls = abi.encodePacked(
+ packedCalls,
+ // 3) checkTransaction
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(instance), // to
+ uint256(0), // value
+ uint256(checkTransactionAction.length), // data length
+ bytes(checkTransactionAction) // data
+ );
+
+ multiSendData = abi.encodeWithSignature("multiSend(bytes)", packedCalls);
+
+ // get the safe tx hash and have the signers sign it
+ safeTxHash = safe.getTransactionHash(
+ defaultDelegatecallTargets[0], // to an approved delegatecall target
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ // not using the refunders
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ safe.nonce()
+ );
+
+ // signers sign the tx
+ bytes memory sigs = _createNSigsForTx(safeTxHash, 2);
+
+ // submit the tx to the safe, expecting a revert
+ vm.prank(signerAddresses[0]);
+ vm.expectRevert("GS013");
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ sigs
+ );
+
+ // the fallback should be different
+ assertEq(
+ SafeManagerLib.getSafeFallbackHandler(safe), goodFallbackHandler, "fallbackHandler should be the same as before"
+ );
+ }
+
+ function test_revert_callExecTransactionFromModuleInsideMultisend() public {
+ // start with 3 valid signers
+ _addSignersSameHat(3, signerHat);
+
+ // the attacker crafts a multisend tx that contains four actions:
+ // 1) HSG.checkAfterExecution — this will reset the _inSafeExecTransaction flag to false after it was set to
+ // true in checkTransaction
+ // 2) Safe.setFallbackHandler to set the maliciousFallbackHandler as the fallback
+ // 3) HSG.execTransactionFromModule — this will update the Safe state snapshot
+ // But the outer call to HSG.checkAfterExecution will revert because the _inSafeExecTransaction flag is false
+
+ // simplest version of (3) requires that the safe has somehow been enabled as a module on HSG. Otherwise, it will
+ // revert with a NotAuthorized() error.
+ vm.prank(owner);
+ instance.enableModule(address(safe));
+
+ // (3) craft an empty execTransactionFromModule call
+ bytes memory execTransactionFromModuleAction = abi.encodeWithSignature(
+ "execTransactionFromModule(address,uint256,bytes,uint8)", address(0), 0, hex"", Enum.Operation.Call
+ );
+
+ // 4) HSG.checkTransaction
+ checkTransactionAction = abi.encodeWithSignature(
+ CHECK_TRANSACTION_SIGNATURE,
+ address(0),
+ 0,
+ hex"",
+ Enum.Operation.Call,
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ _createNContractSigs(2), // attacker's spoofed signatures
+ address(safe)
+ );
+
+ // bundle the three actions into a multisend
+ packedCalls = abi.encodePacked(
+ // 1) checkAfterExecution
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(instance), // to
+ uint256(0), // value
+ uint256(checkAfterExecutionAction.length), // data length
+ bytes(checkAfterExecutionAction), // data
+ // 2) setFallback
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(safe), // to
+ uint256(0), // value
+ uint256(setFallbackAction.length), // data length
+ bytes(setFallbackAction) // data
+ );
+
+ // workaround to avoid stack too deep error
+ packedCalls = abi.encodePacked(
+ packedCalls,
+ // 3) execTransactionFromModule
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(instance), // to
+ uint256(0), // value
+ uint256(execTransactionFromModuleAction.length), // data length
+ bytes(execTransactionFromModuleAction) // data
+ );
+
+ multiSendData = abi.encodeWithSignature("multiSend(bytes)", packedCalls);
+
+ // get the safe tx hash
+ safeTxHash = safe.getTransactionHash(
+ defaultDelegatecallTargets[0], // to an approved delegatecall target
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ // not using the refunders
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ safe.nonce()
+ );
+
+ // signers sign the tx
+ signatures = _createNSigsForTx(safeTxHash, 2);
+
+ // submit the tx to the safe, expecting a revert
+ vm.prank(signerAddresses[0]);
+ // since the revert comes from the outer call to HSG.checkAfterExecution, we expect NoReentryAllowed because the
+ // call to checkAfterExecution comes after the error catching in Safe.execTransaction
+ vm.expectRevert(IHatsSignerGate.NoReentryAllowed.selector);
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ // not using the refunders
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+
+ // the fallback should the same
+ assertEq(
+ SafeManagerLib.getSafeFallbackHandler(safe), goodFallbackHandler, "fallbackHandler should be the same as before"
+ );
+ }
+
+ function test_revert_callExecTransactionFromModuleInsideMultisendWithCheckTransaction() public {
+ // start with 3 valid signers
+ _addSignersSameHat(3, signerHat);
+
+ // the attacker crafts a multisend tx that contains four actions:
+ // 1) HSG.checkAfterExecution — this will reset the _inSafeExecTransaction flag to false after it was set to
+ // true in checkTransaction
+ // 2) Safe.setFallbackHandler to set the maliciousFallbackHandler as the fallback
+ // 3) HSG.execTransactionFromModule — this will update the Safe state snapshot
+ // 4) HSG.checkTransaction — this will set the _inSafeExecTransaction flag to true. But this should revert
+ // because the nonce checks prevent checkTransaction from being called more than once
+
+ // simplest version of (3) requires that the safe has somehow been enabled as a module on HSG. Otherwise, it will
+ // revert with a NotAuthorized() error.
+ vm.prank(owner);
+ instance.enableModule(address(safe));
+
+ // (3) craft an empty execTransactionFromModule call
+ bytes memory execTransactionFromModuleAction = abi.encodeWithSignature(
+ "execTransactionFromModule(address,uint256,bytes,uint8)", address(0), 0, hex"", Enum.Operation.Call
+ );
+
+ // 4) HSG.checkTransaction
+ checkTransactionAction = abi.encodeWithSignature(
+ CHECK_TRANSACTION_SIGNATURE,
+ address(0),
+ 0,
+ hex"",
+ Enum.Operation.Call,
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ _createNContractSigs(2), // attacker's spoofed signatures
+ address(safe)
+ );
+
+ // bundle the three actions into a multisend
+ packedCalls = abi.encodePacked(
+ // 1) checkAfterExecution
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(instance), // to
+ uint256(0), // value
+ uint256(checkAfterExecutionAction.length), // data length
+ bytes(checkAfterExecutionAction), // data
+ // 2) setFallback
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(safe), // to
+ uint256(0), // value
+ uint256(setFallbackAction.length), // data length
+ bytes(setFallbackAction) // data
+ );
+
+ // workaround to avoid stack too deep error
+ packedCalls = abi.encodePacked(
+ packedCalls,
+ // 3) execTransactionFromModule
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(instance), // to
+ uint256(0), // value
+ uint256(execTransactionFromModuleAction.length), // data length
+ bytes(execTransactionFromModuleAction), // data
+ // 4) checkTransaction
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(instance), // to
+ uint256(0), // value
+ uint256(checkTransactionAction.length), // data length
+ bytes(checkTransactionAction) // data
+ );
+
+ multiSendData = abi.encodeWithSignature("multiSend(bytes)", packedCalls);
+
+ // get the safe tx hash
+ safeTxHash = safe.getTransactionHash(
+ defaultDelegatecallTargets[0], // to an approved delegatecall target
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ // not using the refunders
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ safe.nonce()
+ );
+
+ // signers sign the tx
+ signatures = _createNSigsForTx(safeTxHash, 2);
+
+ // submit the tx to the safe, expecting a revert
+ vm.prank(signerAddresses[0]);
+ // We expect GS013 since Safe.execTransaction catches the error NoReentryAllowed error thrown by
+ // HSG.checkTransaction
+ vm.expectRevert("GS013");
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ // not using the refunders
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+
+ // the fallback should the same
+ assertEq(
+ SafeManagerLib.getSafeFallbackHandler(safe), goodFallbackHandler, "fallbackHandler should be the same as before"
+ );
+ }
+
+ function test_revert_changeFallbackHandlerViaExecTransactionFromModuleInsideMultisend() public {
+ // start with 3 valid signers
+ _addSignersSameHat(3, signerHat);
+
+ // the attacker crafts a multisend tx that contains two actions:
+ // 1) HSG.checkAfterExecution — this will reset the _inSafeExecTransaction flag to false after it was set to
+ // true in checkTransaction
+ // 2) HSG.execTransactionFromModule with a payload that changes the fallback handler inside of a multisend. This
+ // should revert because changing safe state is not allowed.
+
+ // simplest version of (2) requires that the safe has somehow been enabled as a module on HSG. Otherwise, it will
+ // revert with a NotAuthorized() error.
+ vm.prank(owner);
+ instance.enableModule(address(safe));
+
+ // (2) craft the execTransactionFromModule action
+ packedCalls = abi.encodePacked(
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(safe), // to
+ uint256(0), // value
+ uint256(setFallbackAction.length), // data length
+ bytes(setFallbackAction) // data
+ );
+
+ multiSendData = abi.encodeWithSignature("multiSend(bytes)", packedCalls);
+
+ bytes memory execTransactionFromModuleAction = abi.encodeWithSignature(
+ "execTransactionFromModule(address,uint256,bytes,uint8)",
+ defaultDelegatecallTargets[0],
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall
+ );
+
+ // bundle the two actions into a multisend
+ packedCalls = abi.encodePacked(
+ // 1) checkAfterExecution
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(instance), // to
+ uint256(0), // value
+ uint256(checkAfterExecutionAction.length), // data length
+ bytes(checkAfterExecutionAction), // data
+ // 2) execTransactionFromModule
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(instance), // to
+ uint256(0), // value
+ uint256(execTransactionFromModuleAction.length), // data length
+ bytes(execTransactionFromModuleAction) // data
+ );
+
+ multiSendData = abi.encodeWithSignature("multiSend(bytes)", packedCalls);
+
+ // get the safe tx hash
+ safeTxHash = safe.getTransactionHash(
+ defaultDelegatecallTargets[0], // to an approved delegatecall target
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ // not using the refunders
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ safe.nonce()
+ );
+
+ // signers sign the tx
+ signatures = _createNSigsForTx(safeTxHash, 2);
+
+ // submit the tx to the safe, expecting a revert
+ // We expect GS013 since Safe.execTransaction catches the CannotChangeFallbackHandler error thrown by
+ // HSG.execTransactionFromModule
+ vm.expectRevert("GS013");
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ // not using the refunders
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+
+ // the fallback should the same
+ assertEq(
+ SafeManagerLib.getSafeFallbackHandler(safe), goodFallbackHandler, "fallbackHandler should be the same as before"
+ );
+ }
+}
diff --git a/test/HatsSignerGate.internals.t.sol b/test/HatsSignerGate.internals.t.sol
new file mode 100644
index 0000000..73774cc
--- /dev/null
+++ b/test/HatsSignerGate.internals.t.sol
@@ -0,0 +1,882 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >=0.8.13;
+
+import { Test, console2 } from "../lib/forge-std/src/Test.sol";
+import { Enum, WithHSGHarnessInstanceTest } from "./TestSuite.t.sol";
+import { IHatsSignerGate } from "../src/interfaces/IHatsSignerGate.sol";
+import { SafeManagerLib } from "../src/lib/SafeManagerLib.sol";
+
+contract AuthInternals is WithHSGHarnessInstanceTest {
+ function test_happy_checkOwner() public {
+ vm.prank(owner);
+ harness.exposed_checkOwner();
+ }
+
+ function test_revert_checkOwner_notOwner() public {
+ for (uint256 i; i < fuzzingAddresses.length; i++) {
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.NotOwnerHatWearer.selector));
+ vm.prank(fuzzingAddresses[i]);
+ harness.exposed_checkOwner();
+ }
+ }
+
+ function test_happy_checkUnlocked() public view {
+ // the harness starts out as unlocked, so this call should not revert
+ harness.exposed_checkUnlocked();
+ }
+
+ function test_revert_checkUnlocked_locked() public {
+ // lock the harness
+ harness.exposed_lock();
+
+ // confirm that its locked
+ assertEq(harness.locked(), true, "harness should be locked");
+
+ // checkUnlocked should revert
+ vm.expectRevert(IHatsSignerGate.Locked.selector);
+ harness.exposed_checkUnlocked();
+ }
+}
+
+contract OwnerSettingsInternals is WithHSGHarnessInstanceTest {
+ function test_lock() public {
+ harness.exposed_lock();
+
+ assertEq(harness.locked(), true, "harness should be locked");
+ }
+
+ function test_fuzz_setOwnerHat(uint256 _newOwnerHat) public {
+ vm.expectEmit();
+ emit IHatsSignerGate.OwnerHatSet(_newOwnerHat);
+ harness.exposed_setOwnerHat(_newOwnerHat);
+
+ assertEq(harness.ownerHat(), _newOwnerHat, "ownerHat should be set to the new ownerHat");
+ }
+
+ function test_fuzz_setClaimableFor(bool _claimableFor) public {
+ vm.expectEmit();
+ emit IHatsSignerGate.ClaimableForSet(_claimableFor);
+ harness.exposed_setClaimableFor(_claimableFor);
+
+ assertEq(harness.claimableFor(), _claimableFor, "claimableFor should be set to the new claimableFor");
+ }
+
+ function test_fuzz_addSignerHats(uint8 _numHats) public {
+ // Bound number of hats to a semi-reasonable range
+ uint256 numHats = bound(_numHats, 1, 100);
+
+ // Create array of signer hats
+ uint256[] memory signerHats = _getRandomSignerHats(numHats);
+
+ vm.expectEmit();
+ emit IHatsSignerGate.SignerHatsAdded(signerHats);
+ harness.exposed_addSignerHats(signerHats);
+
+ // Verify each hat was properly registered
+ for (uint256 i; i < signerHats.length; i++) {
+ assertTrue(harness.isValidSignerHat(signerHats[i]), "signerHat should be valid");
+ }
+ }
+
+ function test_addSignerHats_emptyArray() public {
+ uint256[] memory empty = new uint256[](0);
+
+ vm.expectEmit();
+ emit IHatsSignerGate.SignerHatsAdded(empty);
+ harness.exposed_addSignerHats(empty);
+ }
+
+ function test_addSignerHats_duplicateHats() public {
+ uint256 hatToDuplicate = 1;
+ uint256[] memory duplicates = new uint256[](2);
+ duplicates[0] = hatToDuplicate;
+ duplicates[1] = hatToDuplicate;
+
+ vm.expectEmit();
+ emit IHatsSignerGate.SignerHatsAdded(duplicates);
+ harness.exposed_addSignerHats(duplicates);
+
+ assertTrue(harness.isValidSignerHat(hatToDuplicate), "signerHat should be valid");
+ }
+
+ function test_fuzz_setDelegatecallTarget(uint256 _targetIndex, bool _enabled) public {
+ // bound the target index
+ vm.assume(_targetIndex < fuzzingAddresses.length);
+
+ address target = fuzzingAddresses[_targetIndex];
+
+ // set the delegatecall target
+ vm.expectEmit();
+ emit IHatsSignerGate.DelegatecallTargetEnabled(target, _enabled);
+ harness.exposed_setDelegatecallTarget(target, _enabled);
+
+ // now set it to the opposite enabled state
+ vm.expectEmit();
+ emit IHatsSignerGate.DelegatecallTargetEnabled(target, !_enabled);
+ harness.exposed_setDelegatecallTarget(target, !_enabled);
+ }
+
+ function test_fuzz_setThresholdConfig_valid(uint8 _type, uint120 _min, uint120 _target) public {
+ // ensure the threshold type is valid
+ vm.assume(uint8(_type) < 2);
+ IHatsSignerGate.TargetThresholdType targetType = IHatsSignerGate.TargetThresholdType(_type);
+
+ // ensure the min is valid
+ vm.assume(_min > 0);
+
+ // ensure the target is valid
+ if (targetType == IHatsSignerGate.TargetThresholdType.ABSOLUTE) {
+ vm.assume(_target >= _min);
+ } else {
+ vm.assume(_target <= 10_000);
+ }
+
+ IHatsSignerGate.ThresholdConfig memory config =
+ IHatsSignerGate.ThresholdConfig({ thresholdType: targetType, min: _min, target: _target });
+
+ vm.expectEmit();
+ emit IHatsSignerGate.ThresholdConfigSet(config);
+ harness.exposed_setThresholdConfig(config);
+
+ assertEq(
+ abi.encode(harness.thresholdConfig()),
+ abi.encode(config),
+ "thresholdConfig should be set to the new thresholdConfig"
+ );
+ }
+
+ function test_fuzz_revert_setThresholdConfig_invalidMin(uint8 _type, uint120 _target) public {
+ // ensure the threshold type is valid
+ vm.assume(uint8(_type) < 2);
+ IHatsSignerGate.TargetThresholdType targetType = IHatsSignerGate.TargetThresholdType(_type);
+
+ // ensure the min is invalid
+ uint120 min = 0;
+
+ // ensure the target is valid
+ if (targetType == IHatsSignerGate.TargetThresholdType.ABSOLUTE) {
+ vm.assume(_target >= min);
+ } else {
+ vm.assume(_target <= 10_000);
+ }
+
+ IHatsSignerGate.ThresholdConfig memory config =
+ IHatsSignerGate.ThresholdConfig({ thresholdType: targetType, min: min, target: _target });
+
+ vm.expectRevert(IHatsSignerGate.InvalidThresholdConfig.selector);
+ harness.exposed_setThresholdConfig(config);
+ }
+
+ function test_fuzz_revert_setThresholdConfig_invalidAbsoluteTarget(uint120 _min, uint120 _target) public {
+ // ensure the min is valid
+ vm.assume(_min > 0);
+
+ // ensure the target is invalid
+ vm.assume(_target < _min);
+
+ IHatsSignerGate.ThresholdConfig memory config = IHatsSignerGate.ThresholdConfig({
+ thresholdType: IHatsSignerGate.TargetThresholdType.ABSOLUTE,
+ min: _min,
+ target: _target
+ });
+
+ vm.expectRevert(IHatsSignerGate.InvalidThresholdConfig.selector);
+ harness.exposed_setThresholdConfig(config);
+ }
+
+ function test_fuzz_revert_setThresholdConfig_invalidProportionalTarget(uint120 _min, uint120 _target) public {
+ // ensure the min is valid
+ vm.assume(_min > 0);
+
+ // ensure the target is invalid
+ vm.assume(_target > 10_000);
+
+ IHatsSignerGate.ThresholdConfig memory config = IHatsSignerGate.ThresholdConfig({
+ thresholdType: IHatsSignerGate.TargetThresholdType.PROPORTIONAL,
+ min: _min,
+ target: _target
+ });
+
+ vm.expectRevert(IHatsSignerGate.InvalidThresholdConfig.selector);
+ harness.exposed_setThresholdConfig(config);
+ }
+
+ function test_fuzz_revert_setThresholdConfig_invalidThresholdType(uint8 _type, uint120 _min, uint120 _target) public {
+ // ensure the threshold type is invalid
+ vm.assume(uint8(_type) > 1);
+
+ bytes memory rawConfig = abi.encode(uint8(_type), _min, _target); // thresholdType, min, target
+ bytes memory callData = abi.encodeWithSelector(harness.exposed_setThresholdConfig.selector, rawConfig);
+
+ (bool success,) = address(harness).call(callData);
+
+ assertFalse(success, "setThresholdConfig should revert");
+ }
+}
+
+contract RegisterSignerInternals is WithHSGHarnessInstanceTest {
+ function test_fuzz_happy_registerSigner_allowRegistration(uint256 _hatToRegister, uint8 _signerIndex) public {
+ // bound the signer index and get the signer
+ vm.assume(uint256(_signerIndex) < fuzzingAddresses.length);
+ address signer = fuzzingAddresses[_signerIndex];
+
+ // the hat to register must not be zero
+ vm.assume(_hatToRegister != 0);
+
+ // ensure the hat is a valid signer hat
+ // add the hat to the valid signer hats if it is not already valid
+ if (!harness.isValidSignerHat(_hatToRegister)) {
+ uint256[] memory hats = new uint256[](1);
+ hats[0] = _hatToRegister;
+ harness.exposed_addSignerHats(hats);
+ }
+
+ // ensure the signer is wearing the hat
+ _mockHatWearer(signer, _hatToRegister, true);
+
+ // register the signer, expecting an event
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(_hatToRegister, signer);
+ harness.exposed_registerSigner(_hatToRegister, signer, true);
+
+ assertEq(harness.registeredSignerHats(signer), _hatToRegister, "signer should be registered with the hat");
+ }
+
+ function test_fuzz_happy_registerSigner_disallowRegistration(
+ uint256 _hatToRegister,
+ uint8 _signerIndex,
+ uint256 _registeredHat
+ ) public {
+ // bound the signer index and get the signer
+ vm.assume(uint256(_signerIndex) < fuzzingAddresses.length);
+ address signer = fuzzingAddresses[_signerIndex];
+
+ // the hats should not be zero
+ vm.assume(_hatToRegister != 0);
+ vm.assume(_registeredHat != 0);
+
+ // the hat to register should be different from the registered hat
+ vm.assume(_hatToRegister != _registeredHat);
+
+ // ensure both hats are valid signer hats
+ uint256[] memory hats = new uint256[](2);
+ if (!harness.isValidSignerHat(_hatToRegister)) {
+ hats[0] = _hatToRegister;
+ }
+ if (!harness.isValidSignerHat(_registeredHat)) {
+ hats[1] = _registeredHat;
+ }
+ harness.exposed_addSignerHats(hats); // will not revert if empty
+
+ // ensure the signer is wearing the hat to register
+ _mockHatWearer(signer, _hatToRegister, true);
+
+ // register the signer for the first time
+ _mockHatWearer(signer, _registeredHat, true);
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(_registeredHat, signer);
+ harness.exposed_registerSigner(_registeredHat, signer, false);
+
+ // ensure the signer now loses the registered hat
+ _mockHatWearer(signer, _registeredHat, false);
+
+ // attempt to re-register the signer, expecting a revert
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(_hatToRegister, signer);
+ harness.exposed_registerSigner(_hatToRegister, signer, false);
+
+ assertEq(harness.registeredSignerHats(signer), _hatToRegister, "signer should be registered with the new hat");
+ }
+
+ function test_fuzz_revert_registerSigner_invalidHat(
+ uint256 _hatToRegister,
+ uint8 _signerIndex,
+ bool _allowRegistration
+ ) public {
+ // bound the signer index and get the signer
+ vm.assume(uint256(_signerIndex) < fuzzingAddresses.length);
+ address signer = fuzzingAddresses[_signerIndex];
+
+ // ensure the hat is invalid
+ vm.assume(!harness.isValidSignerHat(_hatToRegister));
+
+ // register the signer, expecting a revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.InvalidSignerHat.selector, _hatToRegister));
+ harness.exposed_registerSigner(_hatToRegister, signer, _allowRegistration);
+
+ assertEq(harness.registeredSignerHats(signer), 0, "signer should not be registered");
+ }
+
+ function test_fuzz_revert_registerSigner_notSignerHatWearer(
+ uint256 _hatToRegister,
+ uint8 _signerIndex,
+ bool _allowRegistration
+ ) public {
+ // bound the signer index and get the signer
+ vm.assume(uint256(_signerIndex) < fuzzingAddresses.length);
+ address signer = fuzzingAddresses[_signerIndex];
+
+ // ensure the hat is valid
+ if (!harness.isValidSignerHat(_hatToRegister)) {
+ uint256[] memory hats = new uint256[](1);
+ hats[0] = _hatToRegister;
+ harness.exposed_addSignerHats(hats);
+ }
+
+ // ensure the signer is not wearing the hat
+ _mockHatWearer(signer, _hatToRegister, false);
+
+ // register the signer, expecting a revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.NotSignerHatWearer.selector, signer));
+ harness.exposed_registerSigner(_hatToRegister, signer, _allowRegistration);
+ }
+
+ function test_fuzz_revert_registerSigner_reregistrationNotAllowed_wearingRegisteredHat(
+ uint256 _hatToRegister,
+ uint8 _signerIndex,
+ uint256 _registeredHat
+ ) public {
+ // bound the signer index and get the signer
+ vm.assume(uint256(_signerIndex) < fuzzingAddresses.length);
+ address signer = fuzzingAddresses[_signerIndex];
+
+ // the hats should not be zero
+ vm.assume(_hatToRegister != 0);
+ vm.assume(_registeredHat != 0);
+
+ // the hat to register should be different from the registered hat
+ vm.assume(_hatToRegister != _registeredHat);
+
+ // ensure both hats are valid signer hats
+ uint256[] memory hats = new uint256[](2);
+ if (!harness.isValidSignerHat(_hatToRegister)) {
+ hats[0] = _hatToRegister;
+ }
+ if (!harness.isValidSignerHat(_registeredHat)) {
+ hats[1] = _registeredHat;
+ }
+ harness.exposed_addSignerHats(hats); // will not revert if empty
+
+ // ensure the signer is wearing the hat to register
+ _mockHatWearer(signer, _hatToRegister, true);
+
+ // ensure the signer is wearing the registered hat
+ _mockHatWearer(signer, _registeredHat, true);
+
+ // register the signer for the first time
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(_registeredHat, signer);
+ harness.exposed_registerSigner(_registeredHat, signer, false);
+
+ // register the signer, expecting a revert since they are still wearing their registered hat
+ vm.expectRevert(IHatsSignerGate.ReregistrationNotAllowed.selector);
+ harness.exposed_registerSigner(_hatToRegister, signer, false);
+ }
+}
+
+contract AddingSignerInternals is WithHSGHarnessInstanceTest {
+ function test_fuzz_addSigner_happy(uint8 _numExistingSigners, uint8 _newSignerIndex) public {
+ // Bound the new signer index
+ vm.assume(uint256(_newSignerIndex) < fuzzingAddresses.length);
+
+ // add random existing signers
+ _addRandomSigners(_numExistingSigners);
+
+ // Cache the existing owner count and threshold
+ uint256 existingThreshold = safe.getThreshold();
+ uint256 existingOwnerCount = safe.getOwners().length;
+
+ // Get the new signer
+ address newSigner = fuzzingAddresses[_newSignerIndex];
+
+ // Check if the new signer is already an owner
+ bool isExistingSigner = safe.isOwner(newSigner);
+
+ // Add the new signer
+ harness.exposed_addSigner(newSigner);
+
+ assertTrue(safe.isOwner(newSigner), "new signer should be added to the safe");
+ assertFalse(safe.isOwner(address(harness)), "the harness should no longer be an owner");
+
+ if (isExistingSigner) {
+ assertEq(safe.getOwners().length, existingOwnerCount, "there shouldn't be additional owners");
+ assertEq(safe.getThreshold(), existingThreshold, "the safe threshold should not change");
+ } else {
+ assertEq(safe.getOwners().length, existingOwnerCount + 1, "there should be one more owner");
+ uint256 correctThreshold = harness.exposed_getNewThreshold(safe.getOwners().length);
+ assertEq(safe.getThreshold(), correctThreshold, "the safe threshold should be correct");
+ }
+ }
+
+ function test_fuzz_addSigner_firstSigner(uint8 _newSignerIndex) public {
+ // bound the new signer index and get the new signer
+ vm.assume(uint256(_newSignerIndex) < fuzzingAddresses.length);
+ address newSigner = fuzzingAddresses[_newSignerIndex];
+
+ // add the new signer
+ harness.exposed_addSigner(newSigner);
+
+ assertEq(safe.getOwners().length, 1, "there should be one owner");
+ assertEq(safe.getThreshold(), 1, "the safe threshold should be one");
+ }
+
+ function test_fuzz_addSigner_secondSigner_notSigner(uint8 _existingSignerIndex, uint8 _newSignerIndex) public {
+ // bound the existing and new signer indices and get the existing and new signers
+ vm.assume(uint256(_existingSignerIndex) < fuzzingAddresses.length);
+ vm.assume(uint256(_newSignerIndex) < fuzzingAddresses.length);
+ address existingSigner = fuzzingAddresses[_existingSignerIndex];
+ address newSigner = fuzzingAddresses[_newSignerIndex];
+
+ // ensure the existing and new signers are different
+ vm.assume(existingSigner != newSigner);
+
+ // setup: add the existing signer
+ harness.exposed_addSigner(existingSigner);
+
+ // cache the existing owner count
+ uint256 existingOwnerCount = safe.getOwners().length;
+
+ // test: add the new signer
+ harness.exposed_addSigner(newSigner);
+
+ assertEq(safe.getOwners().length, existingOwnerCount + 1, "there should be one more owner");
+ uint256 correctThreshold = harness.exposed_getNewThreshold(safe.getOwners().length);
+ assertEq(safe.getThreshold(), correctThreshold, "the safe threshold should be correct");
+ }
+
+ function test_fuzz_addSigner_alreadySigner(uint8 _signerIndex) public {
+ // bound the signer index and get the signer
+ vm.assume(uint256(_signerIndex) < fuzzingAddresses.length);
+ address signer = fuzzingAddresses[_signerIndex];
+
+ // add the signer
+ harness.exposed_addSigner(signer);
+
+ assertEq(safe.getOwners().length, 1, "there should be one owner");
+ assertEq(safe.getThreshold(), 1, "the safe threshold should be one");
+
+ // try to add the signer again, expecting no change
+ harness.exposed_addSigner(signer);
+
+ assertEq(safe.getOwners().length, 1, "there should be one owner");
+ assertEq(safe.getThreshold(), 1, "the safe threshold should be one");
+ }
+}
+
+contract RemovingSignerInternals is WithHSGHarnessInstanceTest {
+ function test_fuzz_removeSigner(uint8 _numExistingSigners) public {
+ // Add random existing signers
+ _addRandomSigners(_numExistingSigners);
+
+ // cache the existing owner count
+ uint256 existingOwnerCount = safe.getOwners().length;
+
+ // randomly select an index to remove
+ uint256 indexToRemove = uint256(keccak256(abi.encode(vm.randomUint(), "remove"))) % existingOwnerCount;
+ address signerToRemove = safe.getOwners()[indexToRemove];
+
+ // test: remove the signer
+ harness.exposed_removeSigner(signerToRemove);
+
+ assertFalse(safe.isOwner(signerToRemove), "the signer should no longer be an owner");
+
+ uint256 expectedOwnerCount = existingOwnerCount == 1 ? 1 : existingOwnerCount - 1;
+ assertEq(safe.getOwners().length, expectedOwnerCount, "the owner count should decrease by one");
+
+ uint256 correctThreshold = harness.exposed_getNewThreshold(safe.getOwners().length);
+ assertEq(safe.getThreshold(), correctThreshold, "the safe threshold should be correct");
+ }
+
+ function test_fuzz_removeSigner_lastSigner(uint8 _signerIndex) public {
+ // bound the signer index and get the signer
+ vm.assume(uint256(_signerIndex) < fuzzingAddresses.length);
+ address signer = fuzzingAddresses[_signerIndex];
+
+ // setup: add the signer
+ harness.exposed_addSigner(signer);
+
+ // remove the signer
+ harness.exposed_removeSigner(signer);
+
+ assertFalse(safe.isOwner(signer), "the signer should no longer be an owner");
+ assertEq(safe.getOwners().length, 1, "there should a single owner");
+ assertEq(safe.getThreshold(), 1, "the safe threshold should be one");
+ assertTrue(safe.isOwner(address(harness)), "the harness should be the owner");
+ }
+
+ function test_fuzz_revert_removeSigner_notSigner(uint8 _signerIndex) public {
+ // bound the signer index and get the signer
+ vm.assume(uint256(_signerIndex) < fuzzingAddresses.length);
+ address signer = fuzzingAddresses[_signerIndex];
+
+ // try to remove the signer, expecting a revert
+ vm.expectRevert(SafeManagerLib.SafeTransactionFailed.selector);
+ harness.exposed_removeSigner(signer);
+
+ assertEq(safe.getOwners().length, 1, "there should a single owner");
+ assertEq(safe.getThreshold(), 1, "the safe threshold should be one");
+ assertTrue(safe.isOwner(address(harness)), "the harness should be the owner");
+ }
+}
+
+contract TransactionValidationInternals is WithHSGHarnessInstanceTest {
+ function test_fuzz_checkModuleTransaction_callToNonSafeTarget(uint8 _toIndex) public {
+ // bound the to index and get the to address
+ vm.assume(uint256(_toIndex) < fuzzingAddresses.length);
+ address to = fuzzingAddresses[_toIndex];
+
+ // test: _checkModuleTransaction should not revert
+ harness.exposed_checkModuleTransaction(to, Enum.Operation.Call, safe);
+ }
+
+ function test_fuzz_checkModuleTransaction_delegatecallToApprovedTarget(
+ uint8 _toIndex,
+ uint8 _numExistingSigners,
+ uint8 _type,
+ uint8 _min,
+ uint16 _target
+ ) public {
+ // bound the to index and get the to address
+ vm.assume(uint256(_toIndex) < fuzzingAddresses.length);
+ address to = fuzzingAddresses[_toIndex];
+
+ // enable the target
+ harness.exposed_setDelegatecallTarget(to, true);
+ assertTrue(harness.enabledDelegatecallTargets(to), "the target should be enabled");
+
+ // set a new threshold config based on the provided values; this will create a new threshold value to check
+ IHatsSignerGate.TargetThresholdType thresholdType = IHatsSignerGate.TargetThresholdType(bound(_type, 0, 1));
+ IHatsSignerGate.ThresholdConfig memory config = _createValidThresholdConfig(thresholdType, _min, _target);
+ harness.exposed_setThresholdConfig(config);
+
+ // add some existing owners; this will create a new owners hash to check
+ _addRandomSigners(_numExistingSigners);
+
+ // cache the existing owners hash, threshold, and fallback handler
+ bytes32 existingOwnersHash = keccak256(abi.encode(safe.getOwners()));
+ uint256 existingThreshold = safe.getThreshold();
+ address existingFallbackHandler = SafeManagerLib.getSafeFallbackHandler(safe);
+
+ // test: _checkModuleTransaction should not revert
+ harness.exposed_checkModuleTransaction(to, Enum.Operation.DelegateCall, safe);
+
+ // ensure the existing owners hash, threshold, and fallback handler are unchanged
+ assertCorrectTransientState(existingOwnersHash, existingThreshold, existingFallbackHandler);
+ }
+
+ function test_fuzz_revert_checkModuleTransaction_delegatecallToUnapprovedTarget(uint8 _toIndex) public {
+ // bound the to index and get the to address
+ vm.assume(uint256(_toIndex) < fuzzingAddresses.length);
+ address to = fuzzingAddresses[_toIndex];
+
+ // ensure the target is not approved
+ assertFalse(harness.enabledDelegatecallTargets(to), "the target should not be enabled");
+
+ // test: _checkModuleTransaction should revert
+ vm.expectRevert(IHatsSignerGate.DelegatecallTargetNotEnabled.selector);
+ harness.exposed_checkModuleTransaction(to, Enum.Operation.DelegateCall, safe);
+ }
+
+ function test_revert_checkModuleTransaction_callToSafe() public {
+ // test: _checkModuleTransaction should revert
+ vm.expectRevert(IHatsSignerGate.CannotCallSafe.selector);
+ harness.exposed_checkModuleTransaction(address(safe), Enum.Operation.Call, safe);
+ }
+
+ function test_checkSafeState() public {
+ // set the owners hash, fallback handler, and threshold in transient state to bypass those errors
+ harness.setExistingOwnersHash(keccak256(abi.encode(safe.getOwners())));
+ harness.setExistingFallbackHandler(SafeManagerLib.getSafeFallbackHandler(safe));
+ harness.setExistingThreshold(safe.getThreshold());
+
+ // test: _checkSafeState should not revert
+ harness.exposed_checkSafeState(safe);
+ }
+
+ function test_revert_checkSafeState_removesHSGAsGuard() public {
+ // remove the HSG as a guard
+ vm.prank(address(safe));
+ safe.setGuard(address(0));
+ assertFalse(SafeManagerLib.getSafeGuard(safe) == address(this), "the HSG is no longer a guard");
+
+ // test: _checkSafeState should revert
+ vm.expectRevert(IHatsSignerGate.CannotDisableThisGuard.selector);
+ harness.exposed_checkSafeState(safe);
+ }
+
+ function test_revert_checkSafeState_changesThreshold() public {
+ assertEq(harness.existingThreshold(), 0, "cached threshold is 0");
+
+ // test: _checkSafeState should revert since the threshold has not be cached in transient state
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeThreshold.selector));
+ harness.exposed_checkSafeState(safe);
+ }
+
+ function test_revert_checkSafeState_changesOwners() public {
+ // set the threshold in transient state to bypass that error
+ harness.setExistingThreshold(safe.getThreshold());
+
+ assertEq(harness.existingOwnersHash(), bytes32(0), "cached owners hash is 0");
+
+ // test: _checkSafeState should revert since the owners hash has not be cached in transient state
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeOwners.selector));
+ harness.exposed_checkSafeState(safe);
+ }
+
+ function test_revert_checkSafeState_changesFallbackHandler() public { }
+
+ function test_revert_checkSafeState_addsModule(uint256 _moduleIndex) public {
+ // set the owners hash, fallback handler, and threshold in transient state to bypass those errors
+ harness.setExistingOwnersHash(keccak256(abi.encode(safe.getOwners())));
+ harness.setExistingFallbackHandler(SafeManagerLib.getSafeFallbackHandler(safe));
+ harness.setExistingThreshold(safe.getThreshold());
+
+ // enable a new module
+ vm.assume(_moduleIndex < fuzzingAddresses.length);
+ address module = fuzzingAddresses[_moduleIndex];
+ vm.prank(address(safe));
+ safe.enableModule(module);
+ assertTrue(safe.isModuleEnabled(module), "a new module is added");
+
+ // test: _checkSafeState should revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeModules.selector));
+ harness.exposed_checkSafeState(safe);
+ }
+
+ function test_revert_checkSafeState_disablesHSGAsModule() public {
+ // set the owners hash, fallback handler, and threshold in transient state to bypass those errors
+ harness.setExistingOwnersHash(keccak256(abi.encode(safe.getOwners())));
+ harness.setExistingFallbackHandler(SafeManagerLib.getSafeFallbackHandler(safe));
+ harness.setExistingThreshold(safe.getThreshold());
+
+ // disable HSG as a module
+ vm.prank(address(safe));
+ safe.disableModule({ prevModule: SENTINELS, module: address(harness) });
+ assertFalse(safe.isModuleEnabled(address(harness)), "HSG is no longer a module");
+
+ // test: _checkSafeState should revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeModules.selector));
+ harness.exposed_checkSafeState(safe);
+ }
+}
+
+contract ViewInternals is WithHSGHarnessInstanceTest {
+ function test_fuzz_getRequiredValidSignatures_absolute(uint8 _min, uint16 _target, uint16 _ownerCount) public {
+ IHatsSignerGate.ThresholdConfig memory config =
+ _createValidThresholdConfig(IHatsSignerGate.TargetThresholdType.ABSOLUTE, _min, _target);
+
+ // set the threshold config
+ harness.exposed_setThresholdConfig(config);
+
+ // get the required valid signatures
+ uint256 actual = harness.exposed_getRequiredValidSignatures(_ownerCount);
+ uint256 expected = _calcAbsoluteRequiredValidSignatures(_ownerCount, config.min, config.target);
+ // ensure the actual is correct
+ assertEq(actual, expected, "the required valid signatures should be correct");
+ }
+
+ function test_fuzz_getRequiredValidSignatures_absolute_ownerCountIsMin(uint8 _min, uint16 _target) public {
+ IHatsSignerGate.ThresholdConfig memory config =
+ _createValidThresholdConfig(IHatsSignerGate.TargetThresholdType.ABSOLUTE, _min, _target);
+
+ // ensure the ownerCount == the min
+ uint256 ownerCount = config.min;
+
+ // set the threshold config
+ harness.exposed_setThresholdConfig(config);
+
+ // get the required valid signatures
+ uint256 actual = harness.exposed_getRequiredValidSignatures(ownerCount);
+ uint256 expected = config.min;
+ // ensure the actual is correct
+ assertEq(actual, expected, "the required valid signatures should be the min");
+ }
+
+ function test_fuzz_getRequiredValidSignatures_absolute_targetOwnerCount(uint8 _min, uint16 _target) public {
+ IHatsSignerGate.ThresholdConfig memory config =
+ _createValidThresholdConfig(IHatsSignerGate.TargetThresholdType.ABSOLUTE, _min, _target);
+
+ // ensure the _ownerCount is at the target
+ uint256 ownerCount = config.target;
+
+ // set the threshold config
+ harness.exposed_setThresholdConfig(config);
+
+ // get the required valid signatures
+ uint256 actual = harness.exposed_getRequiredValidSignatures(ownerCount);
+ uint256 expected = _calcAbsoluteRequiredValidSignatures(ownerCount, config.min, config.target);
+ // ensure the actual is correct
+ assertEq(actual, expected, "the required valid signatures should be the target");
+ }
+
+ function test_fuzz_getRequiredValidSignatures_proportional(uint8 _min, uint16 _target, uint16 _ownerCount) public {
+ IHatsSignerGate.ThresholdConfig memory config =
+ _createValidThresholdConfig(IHatsSignerGate.TargetThresholdType.PROPORTIONAL, _min, _target);
+
+ // set the threshold config
+ harness.exposed_setThresholdConfig(config);
+
+ // get the required valid signatures
+ uint256 actual = harness.exposed_getRequiredValidSignatures(_ownerCount);
+ console2.log("actual", actual);
+ uint256 expected = _calcProportionalRequiredValidSignatures(_ownerCount, config.min, config.target);
+ console2.log("expected", expected);
+ // ensure the actual is correct
+ assertEq(actual, expected, "the required valid signatures should be correct");
+ }
+
+ function test_fuzz_getRequiredValidSignatures_ownerCountLtMin(uint8 _type, uint8 _min, uint16 _target) public {
+ IHatsSignerGate.TargetThresholdType thresholdType = IHatsSignerGate.TargetThresholdType(bound(_type, 0, 1));
+ IHatsSignerGate.ThresholdConfig memory config = _createValidThresholdConfig(thresholdType, _min, _target);
+
+ // ensure the ownerCount is less than the min
+ // generate a random ownerCount such that ownerCount < min
+ uint256 ownerCount = vm.randomUint() % config.min;
+
+ // set the threshold config
+ harness.exposed_setThresholdConfig(config);
+
+ // get the required valid signatures
+ uint256 actual = harness.exposed_getRequiredValidSignatures(ownerCount);
+ uint256 expected = config.min;
+ // ensure the actual is correct
+ assertEq(actual, expected, "the required valid signatures should be the min");
+ }
+
+ function test_fuzz_getNewThreshold(uint8 _type, uint8 _min, uint16 _target, uint16 _ownerCount) public {
+ IHatsSignerGate.TargetThresholdType thresholdType = IHatsSignerGate.TargetThresholdType(bound(_type, 0, 1));
+ IHatsSignerGate.ThresholdConfig memory config = _createValidThresholdConfig(thresholdType, _min, _target);
+
+ // set the threshold config
+ harness.exposed_setThresholdConfig(config);
+
+ // get the new threshold
+ uint256 actual = harness.exposed_getNewThreshold(_ownerCount);
+
+ // calculate the expected new threshold
+ uint256 requiredSignatures = harness.exposed_getRequiredValidSignatures(_ownerCount);
+ uint256 expected = _ownerCount < requiredSignatures ? _ownerCount : requiredSignatures;
+
+ // ensure the actual is correct
+ assertEq(actual, expected, "the new threshold should be correct");
+ }
+
+ function test_fuzz_getNewThreshold_exceedsOwnerCount(uint8 _type, uint8 _min, uint16 _target) public {
+ IHatsSignerGate.TargetThresholdType thresholdType = IHatsSignerGate.TargetThresholdType(bound(_type, 0, 1));
+ IHatsSignerGate.ThresholdConfig memory config = _createValidThresholdConfig(thresholdType, _min, _target);
+
+ // set the threshold config
+ harness.exposed_setThresholdConfig(config);
+
+ // generate a random owner count that is lower than the min
+ uint256 ownerCount = vm.randomUint() % config.min;
+
+ // get the new threshold
+ uint256 actual = harness.exposed_getNewThreshold(ownerCount);
+ uint256 expected = ownerCount;
+ // ensure the actual is correct
+ assertEq(actual, expected, "the new threshold should be the owner count");
+ }
+
+ function test_fuzz_countValidSigners(uint8 _numSigners) public {
+ // ensure we have between 1 and the number of fuzzing addresses
+ _numSigners = uint8(bound(_numSigners, 1, fuzzingAddresses.length));
+
+ // Create fixed-size arrays
+ address[] memory signers = new address[](_numSigners);
+ bool[] memory used = new bool[](fuzzingAddresses.length);
+ uint256 expectedValidCount;
+
+ console2.log("signerHat", signerHat);
+
+ // Fill signers array with unique addresses
+ uint256 count;
+ uint256 attempts;
+ while (count < _numSigners && attempts < 100) {
+ // Added attempts limit as safety
+ // Generate index using a random uint and current attempt
+ uint256 index = uint256(keccak256(abi.encode(vm.randomUint(), attempts))) % fuzzingAddresses.length;
+
+ if (!used[index]) {
+ used[index] = true;
+ signers[count] = fuzzingAddresses[index];
+
+ // Set validity and track expected count
+ bool isValid = uint256(keccak256(abi.encode(vm.randomUint(), "validity", count))) % 2 == 0;
+ _setSignerValidity(signers[count], signerHat, isValid);
+ if (isValid) {
+ // register the signer
+ harness.exposed_registerSigner(signerHat, signers[count], false);
+ // increment the expected valid count
+ expectedValidCount++;
+ }
+
+ count++;
+ }
+ attempts++;
+ }
+
+ // Verify the count matches expected
+ assertEq(harness.exposed_countValidSigners(signers), expectedValidCount, "valid signer count should match expected");
+ }
+}
+
+contract CountingValidSignaturesInternals is WithHSGHarnessInstanceTest {
+ function test_fuzz_countValidSignatures_contractSignature(uint256 _sigCount) public {
+ // ensure we have between 1 and the number of signer addresses
+ _sigCount = bound(_sigCount, 1, signerAddresses.length);
+
+ // generate random contract signatures
+ (bytes memory signatures, uint256 expectedValidCount) = _generateUniqueNonECDSASignatures(_sigCount, false, harness);
+
+ // test: count the valid signatures
+ uint256 actual = harness.exposed_countValidSignatures(bytes32(0), signatures, _sigCount);
+
+ // ensure the actual is correct
+ assertEq(actual, expectedValidCount, "valid signer count should match expected");
+ }
+
+ function test_fuzz_countValidSignatures_approvedHash(uint256 _sigCount) public {
+ // ensure we have between 1 and the number of signer addresses
+ _sigCount = bound(_sigCount, 1, signerAddresses.length);
+
+ // generate random approved hash signatures
+ (bytes memory signatures, uint256 expectedValidCount) = _generateUniqueNonECDSASignatures(_sigCount, true, harness);
+
+ // test: count the valid signatures
+ uint256 actual = harness.exposed_countValidSignatures(bytes32(0), signatures, _sigCount);
+
+ // ensure the actual is correct
+ assertEq(actual, expectedValidCount, "valid signer count should match expected");
+ }
+
+ function test_fuzz_countValidSignatures_ethSign(bytes32 _dataHash, uint256 _sigCount) public {
+ // ensure we have between 1 and the number of signer addresses
+ _sigCount = bound(_sigCount, 1, signerAddresses.length);
+
+ // generate random eth_sign signatures
+ (bytes memory signatures, uint256 expectedValidCount) =
+ _generateUniqueECDSASignatures(_dataHash, _sigCount, true, harness);
+
+ // test: count the valid signatures
+ uint256 actual = harness.exposed_countValidSignatures(_dataHash, signatures, _sigCount);
+
+ // ensure the actual is correct
+ assertEq(actual, expectedValidCount, "valid signer count should match expected");
+ }
+
+ function test_fuzz_countValidSignatures_default(bytes32 _dataHash, uint256 _sigCount) public {
+ // ensure we have between 1 and the number of signer addresses
+ _sigCount = bound(_sigCount, 1, signerAddresses.length);
+
+ // generate random signatures
+ (bytes memory signatures, uint256 expectedValidCount) =
+ _generateUniqueECDSASignatures(_dataHash, _sigCount, false, harness);
+
+ // test: count the valid signatures
+ uint256 actual = harness.exposed_countValidSignatures(_dataHash, signatures, _sigCount);
+
+ // ensure the actual is correct
+ assertEq(actual, expectedValidCount, "valid signer count should match expected");
+ }
+}
diff --git a/test/HatsSignerGate.moduleTxs.sol b/test/HatsSignerGate.moduleTxs.sol
new file mode 100644
index 0000000..14aa889
--- /dev/null
+++ b/test/HatsSignerGate.moduleTxs.sol
@@ -0,0 +1,702 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.13;
+
+import { Test, console2 } from "../lib/forge-std/src/Test.sol";
+import { Enum, WithHSGInstanceTest, WithHSGHarnessInstanceTest } from "./TestSuite.t.sol";
+import { IHats, IHatsSignerGate } from "../src/interfaces/IHatsSignerGate.sol";
+import { SafeManagerLib } from "../src/lib/SafeManagerLib.sol";
+import { IAvatar } from "../src/lib/zodiac-modified/ModifierUnowned.sol";
+import { IModuleManager } from "../src/lib/safe-interfaces/IModuleManager.sol";
+import { ModifierUnowned } from "../src/lib/zodiac-modified/ModifierUnowned.sol";
+import { MultiSend } from "../lib/safe-smart-account/contracts/libraries/MultiSend.sol";
+
+contract ExecutingFromModuleViaHSG is WithHSGHarnessInstanceTest {
+ address newModule = tstModule1;
+ address recipient = makeAddr("recipient");
+
+ function setUp() public override {
+ super.setUp();
+
+ // enable a new module
+ vm.prank(owner);
+ harness.enableModule(newModule);
+
+ // deal the safe some eth
+ deal(address(safe), 1 ether);
+ }
+
+ function test_happy_executionSuccess() public inSafeExecTransaction(false) inModuleExecTransaction(false) {
+ uint256 preValue = address(safe).balance;
+ uint256 transferValue = 0.3 ether;
+ uint256 postValue = preValue - transferValue;
+
+ // have the new module submit/exec the tx, expecting a success event emission from both hsg and the newModule
+ vm.expectEmit();
+ emit IModuleManager.ExecutionFromModuleSuccess(address(harness));
+ vm.expectEmit();
+ emit IAvatar.ExecutionFromModuleSuccess(newModule);
+ vm.prank(newModule);
+ harness.execTransactionFromModule(recipient, transferValue, hex"00", Enum.Operation.Call);
+
+ // confirm the tx succeeded by checking ETH balance changes
+ assertEq(address(safe).balance, postValue);
+ assertEq(recipient.balance, transferValue);
+
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_happy_executionFailure() public inSafeExecTransaction(false) inModuleExecTransaction(false) {
+ // craft a call to a function that doesn't exist on a contract (we'll use Hats.sol)
+ bytes memory badCall = abi.encodeWithSignature("badCall()");
+
+ // have the new module submit/exec the tx, expecting a failure event emission from both hsg and the newModule
+ vm.expectEmit();
+ emit IModuleManager.ExecutionFromModuleFailure(address(harness));
+ vm.expectEmit();
+ emit IAvatar.ExecutionFromModuleFailure(newModule);
+ vm.prank(newModule);
+ harness.execTransactionFromModule(address(hats), 0, badCall, Enum.Operation.Call);
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_happy_delegateCall() public inSafeExecTransaction(false) inModuleExecTransaction(false) {
+ address target = defaultDelegatecallTargets[0];
+
+ uint256 expectedThreshold = safe.getThreshold();
+ address expectedFallbackHandler = SafeManagerLib.getSafeFallbackHandler(safe);
+ bytes32 expectedOwnersHash = keccak256(abi.encode(safe.getOwners()));
+
+ vm.prank(newModule);
+ harness.execTransactionFromModule(target, 0, hex"00", Enum.Operation.DelegateCall);
+
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: expectedOwnersHash,
+ _existingThreshold: expectedThreshold,
+ _existingFallbackHandler: expectedFallbackHandler,
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_notModule() public inSafeExecTransaction(false) inModuleExecTransaction(false) {
+ uint256 preValue = address(safe).balance;
+ uint256 transferValue = 0.3 ether;
+
+ // have a non-module submit/exec the tx, expecting a revert
+ vm.expectRevert(abi.encodeWithSelector(ModifierUnowned.NotAuthorized.selector, other));
+ vm.prank(other);
+ harness.execTransactionFromModule(recipient, transferValue, hex"00", Enum.Operation.Call);
+
+ // confirm the tx did not succeed by checking ETH balance changes
+ assertEq(address(safe).balance, preValue);
+ assertEq(recipient.balance, 0);
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_moduleCannotCallSafe() public inSafeExecTransaction(false) inModuleExecTransaction(false) {
+ uint256 transferValue = 0.3 ether;
+ // try to send to the safe, expecting a revert
+ vm.expectRevert(IHatsSignerGate.CannotCallSafe.selector);
+ vm.prank(newModule);
+ harness.execTransactionFromModule(address(safe), transferValue, hex"00", Enum.Operation.Call);
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_delegatecallTargetNotEnabled()
+ public
+ inSafeExecTransaction(false)
+ inModuleExecTransaction(false)
+ {
+ address target = makeAddr("target");
+
+ // craft a delegatecall to a non-enabled target
+ bytes memory data = abi.encodeWithSignature("maliciousCall()");
+
+ vm.expectRevert(IHatsSignerGate.DelegatecallTargetNotEnabled.selector);
+ vm.prank(newModule);
+ harness.execTransactionFromModule(target, 0, data, Enum.Operation.DelegateCall);
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_inSafeExecTransaction() public inSafeExecTransaction(true) inModuleExecTransaction(false) {
+ address target = makeAddr("target");
+
+ // craft a call
+ bytes memory data = abi.encodeWithSignature("goodCall()");
+
+ vm.expectRevert(IHatsSignerGate.NoReentryAllowed.selector);
+ vm.prank(newModule);
+ harness.execTransactionFromModule(target, 0, data, Enum.Operation.Call);
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_inModuleExecTransaction() public inSafeExecTransaction(false) inModuleExecTransaction(true) {
+ address target = makeAddr("target");
+
+ // craft a delegatecall to a non-enabled target
+ bytes memory data = abi.encodeWithSignature("goodCall()");
+
+ vm.expectRevert(IHatsSignerGate.NoReentryAllowed.selector);
+ vm.prank(newModule);
+ harness.execTransactionFromModule(target, 0, data, Enum.Operation.Call);
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation.Call,
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+}
+
+contract ExecutingFromModuleReturnDataViaHSG is WithHSGHarnessInstanceTest {
+ address newModule = tstModule1;
+ address recipient = makeAddr("recipient");
+
+ function setUp() public override {
+ super.setUp();
+
+ // enable a new module
+ vm.prank(owner);
+ harness.enableModule(newModule);
+
+ // deal the safe some eth
+ deal(address(safe), 1 ether);
+ }
+
+ function test_happy_executionSuccess() public inSafeExecTransaction(false) inModuleExecTransaction(false) {
+ uint256 preValue = address(safe).balance;
+ uint256 transferValue = 0.3 ether;
+ uint256 postValue = preValue - transferValue;
+
+ // have the new module submit/exec the tx, expecting a success event emission from both hsg and the newModule
+ vm.expectEmit();
+ emit IModuleManager.ExecutionFromModuleSuccess(address(harness));
+ vm.expectEmit();
+ emit IAvatar.ExecutionFromModuleSuccess(newModule);
+ vm.prank(newModule);
+ harness.execTransactionFromModuleReturnData(recipient, transferValue, hex"00", Enum.Operation.Call);
+
+ // confirm the tx succeeded by checking ETH balance changes
+ assertEq(address(safe).balance, postValue);
+ assertEq(recipient.balance, transferValue);
+
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_happy_executionFailure() public inSafeExecTransaction(false) inModuleExecTransaction(false) {
+ // craft a call to a function that doesn't exist on a contract (we'll use Hats.sol)
+ bytes memory badCall = abi.encodeWithSignature("badCall()");
+
+ // have the new module submit/exec the tx, expecting a failure event emission from both hsg and the newModule
+ vm.expectEmit();
+ emit IModuleManager.ExecutionFromModuleFailure(address(harness));
+ vm.expectEmit();
+ emit IAvatar.ExecutionFromModuleFailure(newModule);
+ vm.prank(newModule);
+ harness.execTransactionFromModuleReturnData(address(hats), 0, badCall, Enum.Operation.Call);
+
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_happy_delegateCall() public inSafeExecTransaction(false) inModuleExecTransaction(false) {
+ address target = defaultDelegatecallTargets[0];
+
+ uint256 expectedThreshold = safe.getThreshold();
+ address expectedFallbackHandler = SafeManagerLib.getSafeFallbackHandler(safe);
+ bytes32 expectedOwnersHash = keccak256(abi.encode(safe.getOwners()));
+
+ vm.prank(newModule);
+ harness.execTransactionFromModuleReturnData(target, 0, hex"00", Enum.Operation.DelegateCall);
+
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: expectedOwnersHash,
+ _existingThreshold: expectedThreshold,
+ _existingFallbackHandler: expectedFallbackHandler,
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_notModule() public inSafeExecTransaction(false) inModuleExecTransaction(false) {
+ uint256 preValue = address(safe).balance;
+ uint256 transferValue = 0.3 ether;
+
+ // have a non-module submit/exec the tx, expecting a revert
+ vm.expectRevert(abi.encodeWithSelector(ModifierUnowned.NotAuthorized.selector, other));
+ vm.prank(other);
+ harness.execTransactionFromModuleReturnData(recipient, transferValue, hex"00", Enum.Operation.Call);
+
+ // confirm the tx did not succeed by checking ETH balance changes
+ assertEq(address(safe).balance, preValue);
+ assertEq(recipient.balance, 0);
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_moduleCannotCallSafe() public inSafeExecTransaction(false) inModuleExecTransaction(false) {
+ uint256 transferValue = 0.3 ether;
+ // try to send to the safe, expecting a revert
+ vm.expectRevert(IHatsSignerGate.CannotCallSafe.selector);
+ vm.prank(newModule);
+ harness.execTransactionFromModuleReturnData(address(safe), transferValue, hex"00", Enum.Operation.Call);
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_delegatecallTargetNotEnabled()
+ public
+ inSafeExecTransaction(false)
+ inModuleExecTransaction(false)
+ {
+ address target = makeAddr("target");
+
+ // craft a delegatecall to a non-enabled target
+ bytes memory data = abi.encodeWithSignature("maliciousCall()");
+
+ vm.expectRevert(IHatsSignerGate.DelegatecallTargetNotEnabled.selector);
+ vm.prank(newModule);
+ harness.execTransactionFromModuleReturnData(target, 0, data, Enum.Operation.DelegateCall);
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_inSafeExecTransaction(bool _inModuleExecTransaction)
+ public
+ inSafeExecTransaction(true)
+ inModuleExecTransaction(_inModuleExecTransaction)
+ {
+ address target = makeAddr("target");
+
+ // craft a call
+ bytes memory data = abi.encodeWithSignature("goodCall()");
+
+ vm.expectRevert(IHatsSignerGate.NoReentryAllowed.selector);
+ vm.prank(newModule);
+ harness.execTransactionFromModuleReturnData(target, 0, data, Enum.Operation.Call);
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_inModuleExecTransaction(bool _inSafeExecTransaction)
+ public
+ inSafeExecTransaction(_inSafeExecTransaction)
+ inModuleExecTransaction(true)
+ {
+ address target = makeAddr("target");
+
+ // craft a delegatecall to a non-enabled target
+ bytes memory data = abi.encodeWithSignature("goodCall()");
+
+ vm.expectRevert(IHatsSignerGate.NoReentryAllowed.selector);
+ vm.prank(newModule);
+ harness.execTransactionFromModuleReturnData(target, 0, data, Enum.Operation.Call);
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+}
+
+contract ConstrainingModules is WithHSGInstanceTest {
+ address newModule = tstModule1;
+ address recipient = makeAddr("recipient");
+
+ function setUp() public override {
+ super.setUp();
+
+ // enable a new module
+ vm.prank(owner);
+ instance.enableModule(newModule);
+
+ // deal the safe some eth
+ deal(address(safe), 1 ether);
+ }
+
+ function test_revert_delegateCallTargetNotEnabled() public {
+ address target = makeAddr("target");
+
+ // encode a call that we know will be successful
+ bytes memory data = abi.encodeWithSelector(IHats.isWearerOfHat.selector, signerAddresses[0], signerHat);
+
+ // wrap it in a multisend call
+ bytes memory multisendData = abi.encodePacked(
+ Enum.Operation.Call, // 0 for call; 1 for delegatecall
+ address(hats), // to
+ uint256(0), // value
+ uint256(data.length), // data length
+ data // data
+ );
+
+ // encode the multisend call
+ bytes memory multisendCall = abi.encodeWithSelector(MultiSend.multiSend.selector, multisendData);
+
+ // try to exec the tx from the newModule, expect it to revert
+ vm.expectRevert(IHatsSignerGate.DelegatecallTargetNotEnabled.selector);
+ vm.prank(address(newModule));
+ instance.execTransactionFromModule(target, 0, multisendCall, Enum.Operation.DelegateCall);
+
+ // try to exec the tx from the newModuleReturnData, expect it to revert
+ vm.expectRevert(IHatsSignerGate.DelegatecallTargetNotEnabled.selector);
+ vm.prank(address(newModule));
+ instance.execTransactionFromModuleReturnData(target, 0, multisendCall, Enum.Operation.DelegateCall);
+ }
+
+ function test_revert_modulesCannotDisableModule() public {
+ bytes memory disableModuleData =
+ abi.encodeWithSignature("disableModule(address,address)", SENTINELS, address(instance));
+
+ (bytes memory multisendCall,) = _constructSingleActionMultiSendTx(disableModuleData);
+
+ // try to exec the tx from the newModule, expect it to revert
+ vm.expectRevert(IHatsSignerGate.CannotChangeModules.selector);
+ vm.prank(address(newModule));
+ instance.execTransactionFromModule(defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall);
+
+ // try to exec the tx from the newModuleReturnData, expect it to revert
+ vm.expectRevert(IHatsSignerGate.CannotChangeModules.selector);
+ vm.prank(address(newModule));
+ instance.execTransactionFromModuleReturnData(
+ defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall
+ );
+ }
+
+ function test_revert_modulesCannotDisableGuard() public {
+ bytes memory disableGuardData = abi.encodeWithSignature("setGuard(address)", address(0x0));
+
+ (bytes memory multisendCall,) = _constructSingleActionMultiSendTx(disableGuardData);
+
+ // try to exec the tx from the newModule, expect it to revert
+ vm.expectRevert(IHatsSignerGate.CannotDisableThisGuard.selector);
+ vm.prank(address(newModule));
+ instance.execTransactionFromModule(defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall);
+
+ // try to exec the tx from the newModuleReturnData, expect it to revert
+ vm.expectRevert(IHatsSignerGate.CannotDisableThisGuard.selector);
+ vm.prank(address(newModule));
+ instance.execTransactionFromModuleReturnData(
+ defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall
+ );
+ }
+
+ function test_revert_modulesCannotIncreaseThreshold() public {
+ _addSignersSameHat(3, signerHat);
+
+ uint256 oldThreshold = safe.getThreshold();
+ assertEq(oldThreshold, 2);
+
+ // data to increase the threshold data by 1
+ bytes memory changeThresholdData = abi.encodeWithSignature("changeThreshold(uint256)", oldThreshold + 1);
+
+ (bytes memory multisendCall,) = _constructSingleActionMultiSendTx(changeThresholdData);
+
+ // try to exec the tx from the newModule, expect it to revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeThreshold.selector));
+ vm.prank(address(newModule));
+ instance.execTransactionFromModule(defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall);
+
+ // try to exec the tx from the newModuleReturnData, expect it to revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeThreshold.selector));
+ vm.prank(address(newModule));
+ instance.execTransactionFromModuleReturnData(
+ defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall
+ );
+ }
+
+ function test_revert_modulesCannotDecreaseThreshold() public {
+ _addSignersSameHat(3, signerHat);
+
+ uint256 oldThreshold = safe.getThreshold();
+ assertEq(oldThreshold, 2);
+
+ // data to decrease the threshold data by 1
+ bytes memory changeThresholdData = abi.encodeWithSignature("changeThreshold(uint256)", oldThreshold - 1);
+
+ (bytes memory multisendCall,) = _constructSingleActionMultiSendTx(changeThresholdData);
+
+ // try to exec the tx from the newModule, expect it to revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeThreshold.selector));
+ vm.prank(address(newModule));
+ instance.execTransactionFromModule(defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall);
+
+ // try to exec the tx from the newModuleReturnData, expect it to revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeThreshold.selector));
+ vm.prank(address(newModule));
+ instance.execTransactionFromModuleReturnData(
+ defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall
+ );
+ }
+
+ function test_revert_modulesCannotAddOwners() public {
+ // data for call to add owners
+ bytes memory addOwnerData = abi.encodeWithSignature(
+ "addOwnerWithThreshold(address,uint256)",
+ signerAddresses[9], // newOwner
+ safe.getThreshold() // threshold
+ );
+
+ (bytes memory multisendCall,) = _constructSingleActionMultiSendTx(addOwnerData);
+
+ // try to exec the tx from the newModule, expect it to revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeOwners.selector));
+ vm.prank(address(newModule));
+ instance.execTransactionFromModule(defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall);
+
+ // try to exec the tx from the newModuleReturnData, expect it to revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeOwners.selector));
+ vm.prank(address(newModule));
+ instance.execTransactionFromModuleReturnData(
+ defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall
+ );
+ }
+
+ function test_revert_modulesCannotRemoveOwners() public {
+ _addSignersSameHat(3, signerHat);
+ address toRemove = signerAddresses[2];
+
+ // data for call to remove owners
+ bytes memory removeOwnerData = abi.encodeWithSignature(
+ "removeOwner(address,address,uint256)",
+ _findPrevOwner(safe.getOwners(), toRemove), // prevOwner
+ toRemove, // owner to remove
+ safe.getThreshold() // threshold
+ );
+
+ (bytes memory multisendCall,) = _constructSingleActionMultiSendTx(removeOwnerData);
+
+ // try to exec the tx from the newModule, expect it to revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeOwners.selector));
+ vm.prank(address(newModule));
+ instance.execTransactionFromModule(defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall);
+
+ // try to exec the tx from the newModuleReturnData, expect it to revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeOwners.selector));
+ vm.prank(address(newModule));
+ instance.execTransactionFromModuleReturnData(
+ defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall
+ );
+ }
+
+ function test_revert_modulesCannotSwapOwners() public {
+ _addSignersSameHat(3, signerHat);
+ address toRemove = signerAddresses[2];
+ address toAdd = signerAddresses[9];
+ // data for call to swap owners
+ bytes memory swapOwnerData = abi.encodeWithSignature(
+ "swapOwner(address,address,address)",
+ _findPrevOwner(safe.getOwners(), toRemove), // prevOwner
+ toRemove, // owner to swap
+ toAdd // newOwner
+ );
+
+ (bytes memory multisendCall,) = _constructSingleActionMultiSendTx(swapOwnerData);
+
+ // try to exec the tx from the newModule, expect it to revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeOwners.selector));
+ vm.prank(address(newModule));
+ instance.execTransactionFromModule(defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall);
+
+ // try to exec the tx from the newModuleReturnData, expect it to revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeOwners.selector));
+ vm.prank(address(newModule));
+ instance.execTransactionFromModuleReturnData(
+ defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall
+ );
+ }
+
+ function test_revert_delegatecallTargetNotEnabled() public {
+ address target = makeAddr("target");
+
+ // craft a delegatecall to a non-enabled target
+ bytes memory data = abi.encodeWithSignature("maliciousCall()");
+
+ // try to exec the tx from the newModule, expect it to revert
+ vm.expectRevert(IHatsSignerGate.DelegatecallTargetNotEnabled.selector);
+ vm.prank(address(newModule));
+ instance.execTransactionFromModule(target, 0, data, Enum.Operation.DelegateCall);
+
+ // try to exec the tx from the newModuleReturnData, expect it to revert
+ vm.expectRevert(IHatsSignerGate.DelegatecallTargetNotEnabled.selector);
+ vm.prank(address(newModule));
+ instance.execTransactionFromModuleReturnData(target, 0, data, Enum.Operation.DelegateCall);
+ }
+
+ function test_revert_modulesCannotCallSafe() public {
+ uint256 transferValue = 0.2 ether;
+
+ // give the safe some eth
+ vm.deal(address(safe), transferValue);
+
+ // try to exec the tx from the newModule, expect it to revert
+ vm.expectRevert(IHatsSignerGate.CannotCallSafe.selector);
+ vm.prank(address(newModule));
+ instance.execTransactionFromModule(address(safe), transferValue, hex"00", Enum.Operation.Call);
+
+ // try to exec the tx from the newModuleReturnData, expect it to revert
+ vm.expectRevert(IHatsSignerGate.CannotCallSafe.selector);
+ vm.prank(address(newModule));
+ instance.execTransactionFromModuleReturnData(address(safe), transferValue, hex"00", Enum.Operation.Call);
+ }
+
+ function test_revert_cannotChangeFallbackHandler() public {
+ address newFallbackHandler = makeAddr("newFallbackHandler");
+
+ // data for call to change the fallback handler
+ bytes memory changeFallbackHandlerData = abi.encodeWithSignature("setFallbackHandler(address)", newFallbackHandler);
+
+ (bytes memory multisendCall,) = _constructSingleActionMultiSendTx(changeFallbackHandlerData);
+
+ // try to exec the tx from the newModule, expect it to revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeFallbackHandler.selector));
+ vm.prank(address(newModule));
+ instance.execTransactionFromModule(defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall);
+
+ // try to exec the tx from the newModuleReturnData, expect it to revert
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeFallbackHandler.selector));
+ vm.prank(address(newModule));
+ instance.execTransactionFromModuleReturnData(
+ defaultDelegatecallTargets[0], 0, multisendCall, Enum.Operation.DelegateCall
+ );
+ }
+}
diff --git a/test/HatsSignerGate.signerTxs.sol b/test/HatsSignerGate.signerTxs.sol
new file mode 100644
index 0000000..f98c3b1
--- /dev/null
+++ b/test/HatsSignerGate.signerTxs.sol
@@ -0,0 +1,923 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.13;
+
+import { Test, console2 } from "../lib/forge-std/src/Test.sol";
+import { Enum, WithHSGInstanceTest } from "./TestSuite.t.sol";
+import { IHats, IHatsSignerGate } from "../src/interfaces/IHatsSignerGate.sol";
+import { TestGuard } from "./mocks/TestGuard.sol";
+import { MultiSend } from "../lib/safe-smart-account/contracts/libraries/MultiSend.sol";
+
+contract ExecutingTransactions is WithHSGInstanceTest {
+ event ExecutionSuccess(bytes32 indexed txHash, uint256 payment);
+
+ address payable recipient1 = payable(makeAddr("recipient1"));
+ address payable recipient2 = payable(makeAddr("recipient2"));
+
+ function testExecTxByHatWearers() public {
+ _addSignersSameHat(3, signerHat);
+
+ uint256 preNonce = safe.nonce();
+ uint256 preValue = 1 ether;
+ uint256 transferValue = 0.2 ether;
+ uint256 postValue = preValue - transferValue;
+ address destAddress = signerAddresses[3];
+ // give the safe some eth
+ hoax(address(safe), preValue);
+
+ // create the tx
+ bytes32 txHash = _getTxHash(destAddress, transferValue, Enum.Operation.Call, hex"00", safe);
+
+ // have 3 signers sign it
+ bytes memory signatures = _createNSigsForTx(txHash, 3);
+
+ // have one of the signers submit/exec the tx
+ vm.prank(signerAddresses[0]);
+ safe.execTransaction(
+ destAddress,
+ transferValue,
+ hex"00",
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ // confirm it we executed by checking ETH balance changes
+ assertEq(address(safe).balance, postValue);
+ assertEq(destAddress.balance, transferValue);
+ assertEq(safe.nonce(), preNonce + 1);
+ }
+
+ function testExecTxByNonHatWearersReverts() public {
+ _addSignersSameHat(3, signerHat);
+
+ uint256 preNonce = safe.nonce();
+ uint256 preValue = 1 ether;
+ uint256 transferValue = 0.2 ether;
+ // uint256 postValue = preValue - transferValue;
+ address destAddress = signerAddresses[3];
+ // give the safe some eth
+ hoax(address(safe), preValue);
+ // emit log_uint(address(safe).balance);
+ // create tx to send some eth from safe to wherever
+ // create the tx
+ bytes32 txHash = _getTxHash(destAddress, transferValue, Enum.Operation.Call, hex"00", safe);
+
+ // have 3 signers sign it
+ bytes memory signatures = _createNSigsForTx(txHash, 3);
+
+ // removing the hats from 2 signers
+ _setSignerValidity(signerAddresses[0], signerHat, false);
+ _setSignerValidity(signerAddresses[1], signerHat, false);
+
+ // emit log_uint(address(safe).balance);
+ // have one of the signers submit/exec the tx
+ vm.prank(signerAddresses[0]);
+
+ // vm.expectRevert(abi.encodeWithSelector(BelowMinThreshold.selector, minThreshold, 1));
+ vm.expectRevert(IHatsSignerGate.InsufficientValidSignatures.selector);
+
+ safe.execTransaction(
+ destAddress,
+ transferValue,
+ hex"00",
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+
+ // confirm it was not executed by checking ETH balance changes
+ assertEq(destAddress.balance, 0);
+ assertEq(safe.nonce(), preNonce);
+ }
+
+ function testExecTxByTooFewOwnersReverts() public {
+ // add a legit signer
+ _addSignersSameHat(1, signerHat);
+
+ // set up test values
+ uint256 preNonce = safe.nonce();
+ uint256 preValue = 1 ether;
+ uint256 transferValue = 0.2 ether;
+ // uint256 postValue = preValue - transferValue;
+ address destAddress = signerAddresses[3];
+ // give the safe some eth
+ hoax(address(safe), preValue);
+
+ // have the remaining signer sign it
+ // create the tx
+ bytes32 txHash = _getTxHash(destAddress, transferValue, Enum.Operation.Call, hex"00", safe);
+
+ // have them sign it
+ bytes memory signatures = _createNSigsForTx(txHash, 1);
+
+ // have the legit signer exec the tx
+ vm.prank(signerAddresses[0]);
+
+ vm.expectRevert(IHatsSignerGate.ThresholdTooLow.selector);
+
+ safe.execTransaction(
+ destAddress,
+ transferValue,
+ hex"00",
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+
+ // confirm it was not executed by checking ETH balance changes
+ assertEq(destAddress.balance, 0);
+ assertEq(safe.nonce(), preNonce);
+ }
+
+ function testExecByLessThanMinThresholdReverts() public {
+ _addSignersSameHat(2, signerHat);
+
+ _setSignerValidity(signerAddresses[1], signerHat, false);
+ assertEq(safe.getThreshold(), 2, "threshold should be 2");
+ assertEq(instance.validSignerCount(), 1, "valid signer count should be 1");
+
+ // set up test values
+ // uint256 preNonce = safe.nonce();
+ uint256 preValue = 1 ether;
+ uint256 transferValue = 0.2 ether;
+ // uint256 postValue = preValue - transferValue;
+ address destAddress = signerAddresses[3];
+ // give the safe some eth
+ hoax(address(safe), preValue);
+
+ // have the remaining signer sign it
+ // create the tx
+ bytes32 txHash = _getTxHash(destAddress, transferValue, Enum.Operation.Call, hex"00", safe);
+ // have both signers (1 valid, 1 invalid) sign it
+ bytes memory signatures = _createNSigsForTx(txHash, 2);
+
+ // vm.expectRevert(abi.encodeWithSelector(BelowMinThreshold.selector, minThreshold, 1));
+ vm.expectRevert(IHatsSignerGate.InsufficientValidSignatures.selector);
+ safe.execTransaction(
+ destAddress,
+ transferValue,
+ hex"00",
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ }
+
+ function test_Multi_ExecTxByHatWearers() public {
+ _addSignersDifferentHats(3, signerHats);
+
+ uint256 preNonce = safe.nonce();
+ uint256 preValue = 1 ether;
+ uint256 transferValue = 0.2 ether;
+ uint256 postValue = preValue - transferValue;
+ address destAddress = signerAddresses[3];
+ // give the safe some eth
+ hoax(address(safe), preValue);
+
+ // create the tx
+ bytes32 txHash = _getTxHash(destAddress, transferValue, Enum.Operation.Call, hex"00", safe);
+
+ // have 3 signers sign it
+ bytes memory signatures = _createNSigsForTx(txHash, 3);
+
+ // have one of the signers submit/exec the tx
+ vm.prank(signerAddresses[0]);
+ safe.execTransaction(
+ destAddress,
+ transferValue,
+ hex"00",
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ // confirm it we executed by checking ETH balance changes
+ assertEq(address(safe).balance, postValue);
+ assertEq(destAddress.balance, transferValue);
+ assertEq(safe.nonce(), preNonce + 1);
+ }
+
+ function test_Multi_ExecTxByNonHatWearersReverts() public {
+ _addSignersDifferentHats(3, signerHats);
+
+ uint256 preNonce = safe.nonce();
+ uint256 preValue = 1 ether;
+ uint256 transferValue = 0.2 ether;
+ // uint256 postValue = preValue - transferValue;
+ address destAddress = signerAddresses[3];
+ // give the safe some eth
+ hoax(address(safe), preValue);
+ // emit log_uint(address(safe).balance);
+ // create tx to send some eth from safe to wherever
+ // create the tx
+ bytes32 txHash = _getTxHash(destAddress, transferValue, Enum.Operation.Call, hex"00", safe);
+
+ // have 3 signers sign it
+ bytes memory signatures = _createNSigsForTx(txHash, 3);
+
+ // removing the hats from 2 signers
+ _setSignerValidity(signerAddresses[0], signerHat, false);
+ _setSignerValidity(signerAddresses[1], signerHats[1], false);
+
+ // emit log_uint(address(safe).balance);
+ // have one of the signers submit/exec the tx
+ vm.prank(signerAddresses[0]);
+
+ // vm.expectRevert(abi.encodeWithSelector(BelowMinThreshold.selector, minThreshold, 1));
+ vm.expectRevert(IHatsSignerGate.InsufficientValidSignatures.selector);
+
+ safe.execTransaction(
+ destAddress,
+ transferValue,
+ hex"00",
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+
+ // confirm it was not executed by checking ETH balance changes
+ assertEq(destAddress.balance, 0);
+ assertEq(safe.nonce(), preNonce);
+ }
+
+ function test_happy_delegateCall() public {
+ _addSignersSameHat(2, signerHat);
+
+ // encode a call that we know will be successful
+ bytes memory data = abi.encodeWithSelector(IHats.isWearerOfHat.selector, signerAddresses[0], signerHat);
+
+ // wrap it in a multisend call
+ bytes memory multisendData = abi.encodePacked(
+ Enum.Operation.Call, // 0 for call; 1 for delegatecall
+ address(hats), // to
+ uint256(0), // value
+ uint256(data.length), // data length
+ data // data
+ );
+
+ // encode the multisend call
+ bytes memory multisendCall = abi.encodeWithSelector(MultiSend.multiSend.selector, multisendData);
+
+ // execute the multisend to each of the default delegatecall targets
+ for (uint256 i = 0; i < defaultDelegatecallTargets.length; i++) {
+ // get the tx hash
+ bytes32 txHash = _getTxHash(defaultDelegatecallTargets[i], 0, Enum.Operation.DelegateCall, multisendCall, safe);
+
+ // have the signers sign it
+ bytes memory signatures = _createNSigsForTx(txHash, 2);
+
+ // have one of the signers exec the multisend call
+ vm.expectEmit();
+ emit ExecutionSuccess(txHash, 0);
+ vm.prank(signerAddresses[0]);
+ safe.execTransaction(
+ defaultDelegatecallTargets[i],
+ 0,
+ multisendCall,
+ Enum.Operation.DelegateCall,
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ }
+ }
+
+ function test_happy_multiSend() public {
+ uint256 firstSendAmount = 0.1 ether;
+ uint256 secondSendAmount = 0.2 ether;
+
+ // add 3 signers
+ _addSignersSameHat(3, signerHat);
+
+ // deal the safe some ETH
+ deal(address(safe), 1 ether);
+
+ // craft a multisend action to send eth twice
+ bytes memory packedCalls = abi.encodePacked(
+ // 1) first send
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(recipient1), // to
+ uint256(firstSendAmount), // value
+ uint256(0), // data length
+ hex"", // data
+ // 2) second send
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(recipient2), // to
+ uint256(secondSendAmount), // value
+ uint256(0), // data length
+ hex"" // data
+ );
+
+ bytes memory multiSendData = abi.encodeWithSignature("multiSend(bytes)", packedCalls);
+
+ // get the tx hash
+ bytes32 safeTxHash = safe.getTransactionHash(
+ defaultDelegatecallTargets[0],
+ 0, // value
+ multiSendData, // data
+ Enum.Operation.DelegateCall, // operation
+ 0, // safeTxGas
+ 0, // baseGas
+ 0, // gasPrice
+ address(0), // gasToken
+ payable(address(0)), // refundReceiver
+ safe.nonce() // nonce
+ );
+
+ // sufficient signers sign it
+ bytes memory sigs = _createNSigsForTx(safeTxHash, 2);
+
+ // execute the tx
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multiSendData,
+ Enum.Operation.DelegateCall,
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ sigs
+ );
+
+ // confirm correct balances
+ assertEq(recipient1.balance, firstSendAmount, "wrong recipient1 balance");
+ assertEq(recipient2.balance, secondSendAmount, "wrong recipient2 balance");
+ }
+
+ function test_happy_batchMultiSend(uint256 _batchSize) public {
+ // construct an N-action multisend that will call execTransaction a random number of times
+ uint256 batchSize = bound(_batchSize, 1, 50);
+ // ensure the safe has enough ETH to cover the batch
+ deal(address(safe), batchSize * 1 ether);
+
+ // add 3 signers
+ _addSignersSameHat(3, signerHat);
+
+ uint256 sendAmount = 0.1 ether;
+ bytes32[] memory txHashes = new bytes32[](batchSize);
+ bytes[] memory signatures = new bytes[](batchSize);
+ bytes[] memory actionData = new bytes[](batchSize);
+ bytes memory packedCalls;
+
+ uint256 startingNonce = safe.nonce();
+
+ for (uint256 i; i < batchSize; i++) {
+ // get the tx hash for each action
+ txHashes[i] = safe.getTransactionHash(
+ recipient1,
+ sendAmount, // value
+ hex"", // data
+ Enum.Operation.Call, // operation
+ 0, // safeTxGas
+ 0, // baseGas
+ 0, // gasPrice
+ address(0), // gasToken
+ payable(address(0)), // refundReceiver
+ startingNonce + i // nonce needs to increment for each tx
+ );
+
+ // sufficient signers sign each action
+ signatures[i] = _createNSigsForTx(txHashes[i], 2);
+
+ // encode each Safe.execTransaction call
+ actionData[i] = abi.encodeWithSignature(
+ "execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)",
+ recipient1,
+ sendAmount, // value
+ hex"",
+ Enum.Operation.Call,
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures[i]
+ );
+
+ // encode the action data for multiSend
+ bytes memory packedCall = abi.encodePacked(
+ uint8(0), // 0 for call; 1 for delegatecall
+ address(safe), // to
+ uint256(0), // value
+ uint256(actionData[i].length), // data length
+ actionData[i] // data
+ );
+
+ // append the action data into a multisend call
+ packedCalls = abi.encodePacked(packedCalls, packedCall);
+ }
+
+ // execute the multisend
+ MultiSend(defaultDelegatecallTargets[0]).multiSend(packedCalls);
+
+ // confirm correct balances
+ assertEq(recipient1.balance, sendAmount * batchSize, "wrong recipient1 balance");
+ }
+
+ function test_happy_multiSend2() public {
+ test_happy_batchMultiSend(2);
+ }
+
+ function test_happy_multiSend10() public {
+ test_happy_batchMultiSend(10);
+ }
+
+ function test_happy_multiSend50() public {
+ test_happy_batchMultiSend(50);
+ }
+}
+
+contract ConstrainingSigners is WithHSGInstanceTest {
+ function test_revert_delegateCallTargetNotEnabled() public {
+ address target = makeAddr("target");
+
+ _addSignersSameHat(2, signerHat);
+
+ // encode a call that we know will be successful
+ bytes memory data = abi.encodeWithSelector(IHats.isWearerOfHat.selector, signerAddresses[0], signerHat);
+
+ // wrap it in a multisend call
+ bytes memory multisendData = abi.encodePacked(
+ Enum.Operation.Call, // 0 for call; 1 for delegatecall
+ address(hats), // to
+ uint256(0), // value
+ uint256(data.length), // data length
+ data // data
+ );
+
+ // encode the multisend call
+ bytes memory multisendCall = abi.encodeWithSelector(MultiSend.multiSend.selector, multisendData);
+
+ // get the tx hash
+ bytes32 txHash = _getTxHash(target, 0, Enum.Operation.DelegateCall, multisendCall, safe);
+
+ // have the signers sign it
+ bytes memory signatures = _createNSigsForTx(txHash, 2);
+
+ // have one of the signers exec the multisend call
+ vm.expectRevert(IHatsSignerGate.DelegatecallTargetNotEnabled.selector);
+ vm.prank(signerAddresses[0]);
+ safe.execTransaction(
+ target, 0, multisendCall, Enum.Operation.DelegateCall, 0, 0, 0, address(0), payable(address(0)), signatures
+ );
+ }
+
+ function testCannotDisableModule() public {
+ bytes memory disableModuleData =
+ abi.encodeWithSignature("disableModule(address,address)", SENTINELS, address(instance));
+
+ _addSignersSameHat(2, signerHat);
+
+ (bytes memory multisendCall, bytes32 txHash) = _constructSingleActionMultiSendTx(disableModuleData);
+
+ bytes memory signatures = _createNSigsForTx(txHash, 2);
+
+ vm.expectRevert(IHatsSignerGate.CannotChangeModules.selector);
+
+ // execute tx
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multisendCall,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+
+ // _executeSafeTxFrom(address(this), disableModuleData, safe);
+ }
+
+ function testCannotDisableGuard() public {
+ bytes memory disableGuardData = abi.encodeWithSignature("setGuard(address)", address(0x0));
+
+ _addSignersSameHat(2, signerHat);
+
+ (bytes memory multisendCall, bytes32 txHash) = _constructSingleActionMultiSendTx(disableGuardData);
+
+ bytes memory signatures = _createNSigsForTx(txHash, 2);
+
+ vm.expectRevert(IHatsSignerGate.CannotDisableThisGuard.selector);
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multisendCall,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ }
+
+ function testCannotIncreaseThreshold() public {
+ _addSignersSameHat(3, signerHat);
+
+ uint256 oldThreshold = safe.getThreshold();
+ assertEq(oldThreshold, 2);
+
+ // data to increase the threshold data by 1
+ bytes memory changeThresholdData = abi.encodeWithSignature("changeThreshold(uint256)", oldThreshold + 1);
+
+ (bytes memory multisendCall, bytes32 txHash) = _constructSingleActionMultiSendTx(changeThresholdData);
+
+ bytes memory signatures = _createNSigsForTx(txHash, 2);
+
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeThreshold.selector));
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multisendCall,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ }
+
+ function testCannotDecreaseThreshold() public {
+ _addSignersSameHat(3, signerHat);
+
+ uint256 oldThreshold = safe.getThreshold();
+ assertEq(oldThreshold, 2);
+
+ // data to decrease the threshold data by 1
+ bytes memory changeThresholdData = abi.encodeWithSignature("changeThreshold(uint256)", oldThreshold - 1);
+
+ (bytes memory multisendCall, bytes32 txHash) = _constructSingleActionMultiSendTx(changeThresholdData);
+
+ bytes memory signatures = _createNSigsForTx(txHash, 2);
+
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeThreshold.selector));
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multisendCall,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ }
+
+ function testSignersCannotAddOwners() public {
+ _addSignersSameHat(3, signerHat);
+ // data for call to add owners
+ bytes memory addOwnerData = abi.encodeWithSignature(
+ "addOwnerWithThreshold(address,uint256)",
+ signerAddresses[9], // newOwner
+ safe.getThreshold() // threshold
+ );
+
+ (bytes memory multisendCall, bytes32 txHash) = _constructSingleActionMultiSendTx(addOwnerData);
+
+ bytes memory signatures = _createNSigsForTx(txHash, 2);
+
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeOwners.selector));
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multisendCall,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ }
+
+ function testSignersCannotRemoveOwners() public {
+ _addSignersSameHat(3, signerHat);
+ address toRemove = signerAddresses[2];
+ // data for call to remove owners
+ bytes memory removeOwnerData = abi.encodeWithSignature(
+ "removeOwner(address,address,uint256)",
+ _findPrevOwner(safe.getOwners(), toRemove), // prevOwner
+ toRemove, // owner to remove
+ safe.getThreshold() // threshold
+ );
+
+ (bytes memory multisendCall, bytes32 txHash) = _constructSingleActionMultiSendTx(removeOwnerData);
+
+ bytes memory signatures = _createNSigsForTx(txHash, 2);
+
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeOwners.selector));
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multisendCall,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ }
+
+ function testSignersCannotSwapOwners() public {
+ _addSignersSameHat(3, signerHat);
+ address toRemove = signerAddresses[2];
+ address toAdd = signerAddresses[9];
+ // data for call to swap owners
+ bytes memory swapOwnerData = abi.encodeWithSignature(
+ "swapOwner(address,address,address)",
+ _findPrevOwner(safe.getOwners(), toRemove), // prevOwner
+ toRemove, // owner to swap
+ toAdd // newOwner
+ );
+
+ (bytes memory multisendCall, bytes32 txHash) = _constructSingleActionMultiSendTx(swapOwnerData);
+
+ bytes memory signatures = _createNSigsForTx(txHash, 2);
+
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeOwners.selector));
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multisendCall,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ }
+
+ function test_revert_delegatecallTargetNotEnabled() public {
+ address target = makeAddr("target");
+
+ _addSignersSameHat(2, signerHat);
+
+ // craft a delegatecall to a non-enabled target
+ bytes memory data = abi.encodeWithSignature("maliciousCall()");
+ bytes32 txHash = _getTxHash(target, 0, Enum.Operation.DelegateCall, data, safe);
+ bytes memory signatures = _createNSigsForTx(txHash, 2);
+
+ vm.expectRevert(IHatsSignerGate.DelegatecallTargetNotEnabled.selector);
+ safe.execTransaction(
+ target, 0, data, Enum.Operation.DelegateCall, 0, 0, 0, address(0), payable(address(0)), signatures
+ );
+ }
+
+ function test_revert_cannotCallSafe() public {
+ _addSignersSameHat(3, signerHat);
+
+ uint256 transferValue = 0.2 ether;
+
+ // give the safe some eth
+ hoax(address(safe), transferValue);
+
+ // create the tx
+ bytes32 txHash = _getTxHash(address(safe), transferValue, Enum.Operation.Call, hex"00", safe);
+
+ // have 3 signers sign it
+ bytes memory signatures = _createNSigsForTx(txHash, 3);
+
+ // try to exec the tx, expect it to revert
+ vm.expectRevert(IHatsSignerGate.CannotCallSafe.selector);
+ safe.execTransaction(
+ address(safe),
+ transferValue,
+ hex"00",
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ }
+
+ function test_revert_cannotChangeFallbackHandler() public {
+ address newFallbackHandler = makeAddr("newFallbackHandler");
+
+ _addSignersSameHat(3, signerHat);
+
+ // data to change the fallback handler
+ bytes memory changeFallbackHandlerData = abi.encodeWithSignature("setFallbackHandler(address)", newFallbackHandler);
+
+ (bytes memory multisendCall, bytes32 txHash) = _constructSingleActionMultiSendTx(changeFallbackHandlerData);
+
+ bytes memory signatures = _createNSigsForTx(txHash, 2);
+
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.CannotChangeFallbackHandler.selector));
+ safe.execTransaction(
+ defaultDelegatecallTargets[0],
+ 0,
+ multisendCall,
+ Enum.Operation.DelegateCall,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ }
+}
+
+contract HSGGuarding is WithHSGInstanceTest {
+ uint256 public disallowedValue = 1337;
+ uint256 public goodValue = 9_000_000_000;
+ address public recipient = makeAddr("recipient");
+ uint256 public signerCount = 2;
+
+ function setUp() public override {
+ super.setUp();
+
+ // set it on our hsg instance
+ vm.prank(owner);
+ instance.setGuard(address(tstGuard));
+ assertEq(instance.getGuard(), address(tstGuard), "guard should be tstGuard");
+
+ // deal the safe some eth
+ deal(address(safe), 1 ether);
+
+ // add signerCount number of signers
+ _addSignersSameHat(signerCount, signerHat);
+
+ address[] memory owners = safe.getOwners();
+ assertEq(owners.length, signerCount, "owners should be signerCount");
+ }
+
+ /// @dev a successful transaction should hit the tstGuard's checkTransaction and checkAfterExecution funcs
+ function test_executed() public {
+ uint256 preNonce = safe.nonce();
+ uint256 preValue = address(safe).balance;
+ uint256 transferValue = goodValue;
+ uint256 postValue = preValue - transferValue;
+
+ // create the tx
+ bytes32 txHash = _getTxHash(recipient, transferValue, Enum.Operation.Call, hex"00", safe);
+
+ // have 3 signers sign it
+ bytes memory signatures = _createNSigsForTx(txHash, signerCount);
+
+ // we expect the `sender` param to be the Safe address because the sender param from hsg.checkTransaction is the
+ // Safe address
+ vm.expectEmit();
+ emit TestGuard.PreChecked(address(safe));
+ vm.expectEmit();
+ emit TestGuard.PostChecked(true);
+
+ // have one of the signers submit/exec the tx
+ vm.prank(signerAddresses[0]);
+ safe.execTransaction(
+ recipient,
+ transferValue,
+ hex"00",
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+
+ // confirm the tx succeeded by checking ETH balance changes
+ assertEq(address(safe).balance, postValue);
+ assertEq(recipient.balance, transferValue);
+ assertEq(safe.nonce(), preNonce + 1);
+ }
+
+ // the test guard should revert in checkTransaction
+ function test_revert_checkTransaction() public {
+ // we make this happen by using a bad value in the safe.execTransaction call
+ uint256 preNonce = safe.nonce();
+ uint256 preValue = address(safe).balance;
+ uint256 transferValue = disallowedValue;
+
+ // create the tx
+ bytes32 txHash = _getTxHash(recipient, transferValue, Enum.Operation.Call, hex"00", safe);
+
+ // have 3 signers sign it
+ bytes memory signatures = _createNSigsForTx(txHash, signerCount);
+
+ // we expect the test guard to revert in checkTransaction
+ vm.expectRevert("Cannot send 1337");
+
+ // have one of the signers submit/exec the tx
+ vm.prank(signerAddresses[0]);
+ safe.execTransaction(
+ recipient,
+ transferValue,
+ hex"00",
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+
+ // confirm the tx did not succeed by checking ETH balance changes
+ assertEq(address(safe).balance, preValue);
+ assertEq(recipient.balance, 0);
+ assertEq(safe.nonce(), preNonce);
+ }
+
+ // the test guard should revert in checkAfterExecution
+ function test_revert_checkAfterExecution() public {
+ // we make this happen by setting the test guard to disallow execution
+ tstGuard.disallowExecution();
+
+ // craft a basic eth transfer tx
+ uint256 preNonce = safe.nonce();
+ uint256 preValue = address(safe).balance;
+ uint256 transferValue = goodValue;
+
+ // create the tx
+ bytes32 txHash = _getTxHash(recipient, transferValue, Enum.Operation.Call, hex"00", safe);
+
+ // have 3 signers sign it
+ bytes memory signatures = _createNSigsForTx(txHash, signerCount);
+
+ // we expect the test guard to revert in checkTransaction
+ vm.expectRevert("Reverted in checkAfterExecution");
+
+ // have one of the signers submit/exec the tx
+ vm.prank(signerAddresses[0]);
+ safe.execTransaction(
+ recipient,
+ transferValue,
+ hex"00",
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+
+ // confirm the tx did not succeed by checking ETH balance changes
+ assertEq(address(safe).balance, preValue);
+ assertEq(recipient.balance, 0);
+ assertEq(safe.nonce(), preNonce);
+ }
+}
diff --git a/test/HatsSignerGate.t.sol b/test/HatsSignerGate.t.sol
index 7cab337..76a2343 100644
--- a/test/HatsSignerGate.t.sol
+++ b/test/HatsSignerGate.t.sol
@@ -1,1244 +1,1831 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
-import "./HSGTestSetup.t.sol";
-
-contract HatsSignerGateTest is HSGTestSetup {
- function testSetTargetThreshold() public {
- addSigners(1);
- mockIsWearerCall(address(this), ownerHat, true);
-
- vm.expectEmit(false, false, false, true);
- emit HSGLib.TargetThresholdSet(3);
- hatsSignerGate.setTargetThreshold(3);
+import { Test, console2 } from "../lib/forge-std/src/Test.sol";
+import {
+ Enum, ISafe, TestSuite, WithHSGInstanceTest, WithHSGHarnessInstanceTest, HatsSignerGate
+} from "./TestSuite.t.sol";
+import { IHats, IHatsSignerGate } from "../src/interfaces/IHatsSignerGate.sol";
+import { DeployInstance } from "../script/HatsSignerGate.s.sol";
+import { IAvatar } from "../src/lib/zodiac-modified/ModifierUnowned.sol";
+import { IModuleManager } from "../src/lib/safe-interfaces/IModuleManager.sol";
+import { GuardableUnowned } from "../src/lib/zodiac-modified/GuardableUnowned.sol";
+import { ModifierUnowned } from "../src/lib/zodiac-modified/ModifierUnowned.sol";
+import { TestGuard } from "./mocks/TestGuard.sol";
+import { MultiSend } from "../lib/safe-smart-account/contracts/libraries/MultiSend.sol";
+import { SafeManagerLib } from "../src/lib/SafeManagerLib.sol";
+
+contract ImplementationDeployment is TestSuite {
+ // errors from dependencies
+ error InvalidInitialization();
+
+ function test_constructorArgs() public view {
+ assertEq(address(implementationHSG.HATS()), address(hats));
+
+ (address safe, address fallBack, address multisend, address factory) =
+ implementationHSG.getSafeDeployParamAddresses();
+ assertEq(safe, address(singletonSafe));
+ assertEq(fallBack, address(safeFallbackLibrary));
+ assertEq(multisend, address(safeMultisendLibrary));
+ assertEq(factory, address(safeFactory));
+ }
+
+ function test_version() public view {
+ assertEq(implementationHSG.version(), "2.0.0");
+ }
+
+ function test_ownerHat() public view {
+ assertEq(implementationHSG.ownerHat(), 1);
+ }
+
+ function test_revert_initializerCalledTwice() public {
+ IHatsSignerGate.SetupParams memory setupParams = IHatsSignerGate.SetupParams({
+ ownerHat: ownerHat,
+ signerHats: signerHats,
+ safe: address(safe),
+ thresholdConfig: thresholdConfig,
+ locked: false,
+ claimableFor: false,
+ implementation: address(implementationHSG),
+ hsgGuard: address(tstGuard),
+ hsgModules: tstModules
+ });
+ bytes memory initializeParams = abi.encode(setupParams);
+ vm.expectRevert(InvalidInitialization.selector);
+ implementationHSG.setUp(initializeParams);
+ }
+}
- assertEq(hatsSignerGate.targetThreshold(), 3);
- assertEq(safe.getThreshold(), 1);
+contract InstanceDeployment is TestSuite {
+ // errors from dependencies
+ error InvalidInitialization();
+
+ function test_initialParams_existingSafe(bool _locked, bool _claimableFor) public {
+ // deploy safe with this contract as the single owner
+ address[] memory owners = new address[](1);
+ owners[0] = address(this);
+ ISafe testSafe = _deploySafe(owners, 1, TEST_SALT_NONCE);
+
+ instance = _deployHSG({
+ _ownerHat: ownerHat,
+ _signerHats: signerHats,
+ _thresholdConfig: thresholdConfig,
+ _safe: address(testSafe),
+ _expectedError: bytes4(0), // no expected error
+ _locked: _locked,
+ _claimableFor: _claimableFor,
+ _hsgGuard: address(tstGuard),
+ _hsgModules: tstModules,
+ _verbose: false
+ });
+
+ assertEq(instance.ownerHat(), ownerHat);
+ assertValidSignerHats(instance, signerHats);
+ assertEq(instance.thresholdConfig(), thresholdConfig);
+ assertEq(address(instance.safe()), address(testSafe));
+ assertEq(address(instance.implementation()), address(implementationHSG));
+ assertEq(instance.locked(), _locked);
+ assertEq(instance.claimableFor(), _claimableFor);
+ assertEq(address(instance.getGuard()), address(tstGuard));
+ assertCorrectModules(instance, tstModules);
+ assertEq(address(instance.HATS()), address(hats));
+
+ // check that the default delegatecall targets are enabled
+ for (uint256 i; i < defaultDelegatecallTargets.length; ++i) {
+ assertTrue(instance.enabledDelegatecallTargets(defaultDelegatecallTargets[i]), "default target should be enabled");
}
-
- function testSetTargetThreshold3of4() public {
- addSigners(4);
- mockIsWearerCall(address(this), ownerHat, true);
-
- vm.expectEmit(false, false, false, true);
- emit HSGLib.TargetThresholdSet(3);
-
- hatsSignerGate.setTargetThreshold(3);
-
- assertEq(hatsSignerGate.targetThreshold(), 3);
- assertEq(safe.getThreshold(), 3);
+ }
+
+ function test_initialParams_newSafe(bool _locked, bool _claimableFor) public {
+ (instance, safe) = _deployHSGAndSafe({
+ _ownerHat: ownerHat,
+ _signerHats: signerHats,
+ _thresholdConfig: thresholdConfig,
+ _locked: _locked,
+ _claimableFor: _claimableFor,
+ _hsgGuard: address(tstGuard),
+ _hsgModules: tstModules,
+ _verbose: false
+ });
+
+ assertEq(instance.ownerHat(), ownerHat);
+ assertValidSignerHats(instance, signerHats);
+ assertEq(instance.thresholdConfig(), thresholdConfig);
+ assertEq(address(instance.HATS()), address(hats));
+ assertEq(address(instance.safe()), address(safe));
+ assertEq(address(instance.implementation()), address(implementationHSG));
+ assertEq(_getSafeGuard(address(safe)), address(instance));
+ assertTrue(safe.isModuleEnabled(address(instance)));
+ assertEq(safe.getOwners()[0], address(instance));
+ assertEq(instance.locked(), _locked);
+ assertEq(instance.claimableFor(), _claimableFor);
+ assertEq(address(instance.getGuard()), address(tstGuard));
+ assertCorrectModules(instance, tstModules);
+
+ // check that the default delegatecall targets are enabled
+ for (uint256 i; i < defaultDelegatecallTargets.length; ++i) {
+ assertTrue(instance.enabledDelegatecallTargets(defaultDelegatecallTargets[i]), "default target should be enabled");
}
-
- function testSetTargetThreshold4of4() public {
- addSigners(4);
- mockIsWearerCall(address(this), ownerHat, true);
-
- vm.expectEmit(false, false, false, true);
- emit HSGLib.TargetThresholdSet(4);
-
- hatsSignerGate.setTargetThreshold(4);
-
- assertEq(hatsSignerGate.targetThreshold(), 4);
- assertEq(safe.getThreshold(), 4);
+ }
+
+ function test_deployMultipleInstancesWithSameParams(uint256 _count) public {
+ _count = bound(_count, 1, 20);
+
+ // set up the instance deployer with the deploy params
+ DeployInstance instanceDeployer = new DeployInstance();
+ instanceDeployer.prepare1(
+ address(implementationHSG),
+ ownerHat,
+ signerHats,
+ thresholdConfig,
+ address(0),
+ true,
+ false,
+ address(tstGuard),
+ tstModules
+ );
+
+ HatsSignerGate inst;
+
+ // deploy the instances
+ for (uint256 i; i < _count; ++i) {
+ console2.log("deploying instance", i + 1);
+
+ // set the nonce for the instance deployer
+ instanceDeployer.prepare2(false, i);
+
+ // deploy the instance
+ inst = instanceDeployer.run();
+
+ ISafe s = inst.safe();
+
+ // check that the instance is deployed correctly
+ assertEq(inst.ownerHat(), ownerHat);
+ assertValidSignerHats(inst, signerHats);
+ assertEq(inst.thresholdConfig(), thresholdConfig);
+ assertEq(address(inst.HATS()), address(hats));
+ assertEq(address(inst.implementation()), address(implementationHSG));
+ assertEq(_getSafeGuard(address(s)), address(inst));
+ assertTrue(s.isModuleEnabled(address(inst)));
+ assertEq(s.getOwners()[0], address(inst));
+ assertEq(inst.locked(), true);
+ assertEq(inst.claimableFor(), false);
+ assertEq(address(inst.getGuard()), address(tstGuard));
+ assertCorrectModules(inst, tstModules);
}
+ }
+
+ function test_revert_initializerCalledTwice() public {
+ (instance, safe) = _deployHSGAndSafe({
+ _ownerHat: ownerHat,
+ _signerHats: signerHats,
+ _thresholdConfig: thresholdConfig,
+ _locked: false,
+ _claimableFor: false,
+ _hsgGuard: address(tstGuard),
+ _hsgModules: tstModules,
+ _verbose: false
+ });
+
+ IHatsSignerGate.SetupParams memory setupParams = IHatsSignerGate.SetupParams({
+ ownerHat: ownerHat,
+ signerHats: signerHats,
+ safe: address(safe),
+ thresholdConfig: thresholdConfig,
+ locked: false,
+ claimableFor: false,
+ implementation: address(implementationHSG),
+ hsgGuard: address(tstGuard),
+ hsgModules: tstModules
+ });
+ bytes memory initializeParams = abi.encode(setupParams);
+ // console2.logBytes(initializeParams);
+ vm.expectRevert(InvalidInitialization.selector);
+ instance.setUp(initializeParams);
+ }
+}
- function testNonOwnerHatWearerCannotSetTargetThreshold() public {
- mockIsWearerCall(address(this), ownerHat, false);
-
- vm.expectRevert("UNAUTHORIZED");
-
- hatsSignerGate.setTargetThreshold(3);
-
- assertEq(hatsSignerGate.targetThreshold(), 2);
- assertEq(safe.getThreshold(), 1);
+/// @dev see HatsSignerGate.internals.t.sol:ClaimingSignerInternals for tests of claimSigner internal logic
+contract ClaimingSigner is WithHSGInstanceTest {
+ /// forge-config: default.fuzz.runs = 200
+ function test_fuzz_happy_claimSigner(uint256 _seed) public {
+ uint256 signerHat = _getRandomValidSignerHat(_seed);
+ address caller = _getRandomAddress(_seed);
+ // make caller a valid signer
+ _setSignerValidity(caller, signerHat, true);
+
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(signerHat, caller);
+ vm.prank(caller);
+ instance.claimSigner(signerHat);
+
+ assertEq(instance.registeredSignerHats(caller), signerHat, "caller should have registered the signer hat");
+ assertTrue(instance.isValidSigner(caller), "caller should be a valid signer");
+ assertTrue(safe.isOwner(caller), "caller should be on the safe");
+ }
+
+ function test_fuzz_claimSigner_alreadyRegistered_differentHats(uint256 _seed) public {
+ address caller = _getRandomAddress(_seed);
+ // make caller a valid signer for two valid signer hats
+ _setSignerValidity(caller, signerHats[0], true);
+ _setSignerValidity(caller, signerHats[1], true);
+ // claim the first signer hat
+ vm.prank(caller);
+ instance.claimSigner(signerHats[0]);
+
+ // claim again with a different signer hat
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(signerHats[1], caller);
+ vm.prank(caller);
+ instance.claimSigner(signerHats[1]);
+
+ assertEq(instance.registeredSignerHats(caller), signerHats[1], "caller should have registered the new signer hat");
+ assertTrue(instance.isValidSigner(caller), "caller should be a valid signer");
+ assertTrue(safe.isOwner(caller), "caller should be on the safe");
+ }
+
+ function test_fuzz_claimSigner_alreadyRegistered_sameHat(uint256 _seed) public {
+ // get a random valid signer hat
+ uint256 signerHat = _getRandomValidSignerHat(_seed);
+ address caller = _getRandomAddress(_seed);
+ // make caller a valid signer for the signer hat
+ _setSignerValidity(caller, signerHat, true);
+
+ // claim for the first time
+ vm.prank(caller);
+ instance.claimSigner(signerHat);
+
+ // claim again with the same signer hat
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(signerHat, caller);
+ vm.prank(caller);
+ instance.claimSigner(signerHat);
+
+ assertEq(instance.registeredSignerHats(caller), signerHat, "caller should be registered for the same hat");
+ assertTrue(instance.isValidSigner(caller), "caller should be a valid signer");
+ assertTrue(safe.isOwner(caller), "caller should be on the safe");
+ }
+
+ function test_fuzz_claimSigner_notRegistered_onSafe(uint256 _seed) public {
+ uint256 signerHat = _getRandomValidSignerHat(_seed);
+ address caller = _getRandomAddress(_seed);
+ // make caller a valid signer
+ _setSignerValidity(caller, signerHat, true);
+
+ // add the signer to the safe directly
+ vm.prank(address(safe));
+ safe.addOwnerWithThreshold(caller, 1);
+
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(signerHat, caller);
+ vm.prank(caller);
+ instance.claimSigner(signerHat);
+
+ assertEq(instance.registeredSignerHats(caller), signerHat, "caller should have registered the signer hat");
+ assertTrue(instance.isValidSigner(caller), "caller should be a valid signer");
+ assertTrue(safe.isOwner(caller), "caller should be on the safe");
+ }
+
+ function test_fuzz_revert_invalidSignerHat(uint256 _signerHat, uint256 _seed) public {
+ vm.assume(_signerHat > 0); // the 0 hat id does not exist
+ vm.assume(!instance.isValidSignerHat(_signerHat));
+ address caller = _getRandomAddress(_seed);
+ // make caller a valid signer; we need to use the mock because the signer hat is not real
+ _mockHatWearer(caller, _signerHat, true);
+
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.InvalidSignerHat.selector, _signerHat));
+ vm.prank(caller);
+ instance.claimSigner(_signerHat);
+
+ assertNotEq(instance.registeredSignerHats(caller), _signerHat, "caller should not have registered the signer hat");
+ assertFalse(instance.isValidSigner(caller), "caller should not be a valid signer");
+ assertFalse(safe.isOwner(caller), "caller should not be on the safe");
+ }
+
+ function test_fuzz_revert_notWearingSignerHat(uint256 _seed) public {
+ uint256 signerHat = _getRandomValidSignerHat(_seed);
+ address caller = _getRandomAddress(_seed);
+ // make caller a valid signer; we need to use the mock because the signer hat is not real
+ _setSignerValidity(caller, signerHat, false);
+
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.NotSignerHatWearer.selector, caller));
+ vm.prank(caller);
+ instance.claimSigner(signerHat);
+
+ assertNotEq(instance.registeredSignerHats(caller), signerHat, "caller should not have registered the signer hat");
+ assertFalse(instance.isValidSigner(caller), "caller should not be a valid signer");
+ assertFalse(safe.isOwner(caller), "caller should not be on the safe");
+ }
+
+ function test_fuzz_multipleSigners_multipleHats(uint256 _count, uint256 _seed) public {
+ // bound the count to be between 1 and the number of signer hats
+ _count = bound(_count, 1, signerHats.length);
+
+ for (uint256 i; i < _count; ++i) {
+ uint256 seed = uint256(keccak256(abi.encode(_seed, i)));
+ uint256 signerHat = _getRandomValidSignerHat(seed);
+ address caller = _getRandomAddress(seed);
+ _setSignerValidity(caller, signerHat, true);
+
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(signerHat, caller);
+ vm.prank(caller);
+ instance.claimSigner(signerHat);
+
+ assertEq(instance.registeredSignerHats(caller), signerHat, "caller should have registered the signer hat");
+ assertTrue(instance.isValidSigner(caller), "caller should be a valid signer");
+ assertTrue(safe.isOwner(caller), "caller should be on the safe");
}
+ }
+}
- function testSetMinThreshold() public {
- mockIsWearerCall(address(this), ownerHat, true);
- hatsSignerGate.setTargetThreshold(3);
-
- vm.expectEmit(false, false, false, true);
- emit HSGLib.MinThresholdSet(3);
+contract ClaimingSignerFor is WithHSGInstanceTest {
+ function test_happy_claimSignerFor(uint256 _seed) public isClaimableFor(true) {
+ // get a random valid signer hat
+ uint256 signerHat = _getRandomValidSignerHat(_seed);
+ address signer = _getRandomAddress(_seed);
+ // make signer a valid signer for the signer hat
+ _setSignerValidity(signer, signerHat, true);
+
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(signerHat, signer);
+ instance.claimSignerFor(signerHat, signer);
+
+ assertEq(instance.registeredSignerHats(signer), signerHat, "signer should have registered the signer hat");
+ assertTrue(instance.isValidSigner(signer), "signer should be a valid signer");
+ assertTrue(safe.isOwner(signer), "signer should be on the safe");
+ }
+
+ function test_alreadyOwner_notRegistered(uint256 _seed) public isClaimableFor(true) {
+ // get a random valid signer hat
+ uint256 signerHat = _getRandomValidSignerHat(_seed);
+ address signer = _getRandomAddress(_seed);
+ // add the signer to the safe directly
+ vm.prank(address(safe));
+ safe.addOwnerWithThreshold(signer, 1);
+
+ // make signer a valid signer for the signer hat
+ _setSignerValidity(signer, signerHat, true);
+
+ // claim the signer
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(signerHat, signer);
+ instance.claimSignerFor(signerHat, signer);
+
+ assertEq(instance.registeredSignerHats(signer), signerHat, "signer should have registered the signer hat");
+ assertTrue(instance.isValidSigner(signer), "signer should be a valid signer");
+ assertTrue(safe.isOwner(signer), "signer should be on the safe");
+ }
+
+ function test_revert_notClaimableFor(uint256 _seed) public isClaimableFor(false) {
+ uint256 signerHat = _getRandomValidSignerHat(_seed);
+ address signer = _getRandomAddress(_seed);
+ // make signer a valid signer for the signer hat
+ _setSignerValidity(signer, signerHat, true);
+
+ vm.expectRevert(IHatsSignerGate.NotClaimableFor.selector);
+ instance.claimSignerFor(signerHat, signer);
+
+ assertNotEq(instance.registeredSignerHats(signer), signerHat, "signer should not have registered the signer hat");
+ assertFalse(instance.isValidSigner(signer), "signer should not be a valid signer");
+ assertFalse(safe.isOwner(signer), "signer should not be on the safe");
+ }
+
+ function test_revert_invalidSignerHat(uint256 _signerHat, uint256 _seed) public isClaimableFor(true) {
+ vm.assume(_signerHat > 0); // the 0 hat id does not exist
+ vm.assume(!instance.isValidSignerHat(_signerHat)); // this test is for invalid signer hats
+ address signer = _getRandomAddress(_seed);
+ // make signer a valid signer for the signer hat; we need to use the mock because the signer hat is not real
+ _mockHatWearer(signer, _signerHat, true);
+
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.InvalidSignerHat.selector, _signerHat));
+ instance.claimSignerFor(_signerHat, signer);
+
+ assertNotEq(instance.registeredSignerHats(signer), _signerHat, "signer should not have registered the signer hat");
+ assertFalse(instance.isValidSigner(signer), "signer should not be a valid signer");
+ assertFalse(safe.isOwner(signer), "signer should not be on the safe");
+ }
+
+ function test_revert_notWearingSignerHat(uint256 _seed) public isClaimableFor(true) {
+ uint256 signerHat = _getRandomValidSignerHat(_seed);
+ address signer = _getRandomAddress(_seed);
+ // make signer a invalid signer for the signer hat
+ _setSignerValidity(signer, signerHat, false);
+
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.NotSignerHatWearer.selector, signer));
+ instance.claimSignerFor(signerHat, signer);
+
+ assertNotEq(instance.registeredSignerHats(signer), signerHat, "signer should not have registered the signer hat");
+ assertFalse(instance.isValidSigner(signer), "signer should not be a valid signer");
+ assertFalse(safe.isOwner(signer), "signer should not be on the safe");
+ }
+
+ function test_revert_alreadyRegistered_stillWearingRegisteredHat(uint256 _seed) public isClaimableFor(true) {
+ address signer = _getRandomAddress(_seed);
+ // make signer a valid signer for two signer hats
+ _setSignerValidity(signer, signerHats[0], true);
+ _setSignerValidity(signer, signerHats[1], true);
+
+ // claim for the first time
+ instance.claimSignerFor(signerHats[0], signer);
+
+ // claim again with a different signer hat
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.ReregistrationNotAllowed.selector));
+ instance.claimSignerFor(signerHats[1], signer);
+
+ assertEq(instance.registeredSignerHats(signer), signerHats[0], "signer should have registered the first signer hat");
+ assertTrue(instance.isValidSigner(signer), "signer should still be a valid signer");
+ assertTrue(safe.isOwner(signer), "signer should still be on the safe");
+ }
+
+ function test_alreadyRegistered_notWearingRegisteredHat(uint256 _seed) public isClaimableFor(true) {
+ address signer = _getRandomAddress(_seed);
+ // make signer a valid signer for two signer hats
+ _setSignerValidity(signer, signerHats[0], true);
+ _setSignerValidity(signer, signerHats[1], true);
+
+ // claim for the first time
+ instance.claimSignerFor(signerHats[0], signer);
+
+ // signer loses the first signer hat
+ _setSignerValidity(signer, signerHats[0], false);
+
+ // claim again with the second signer hat
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(signerHats[1], signer);
+ instance.claimSignerFor(signerHats[1], signer);
+
+ assertEq(
+ instance.registeredSignerHats(signer), signerHats[1], "signer should have registered the second signer hat"
+ );
+ assertTrue(instance.isValidSigner(signer), "signer should be a valid signer");
+ assertTrue(safe.isOwner(signer), "signer should be on the safe");
+ }
+}
- hatsSignerGate.setMinThreshold(3);
+contract ClaimingSignersFor is WithHSGInstanceTest {
+ function test_startingEmpty_happy(uint256 _signerCount) public {
+ _signerCount = bound(_signerCount, 1, signerAddresses.length);
- assertEq(hatsSignerGate.minThreshold(), 3);
+ // set up signer validity
+ for (uint256 i; i < _signerCount; ++i) {
+ _setSignerValidity(signerAddresses[i], signerHat, true);
}
- function testSetInvalidMinThreshold() public {
- mockIsWearerCall(address(this), ownerHat, true);
+ // set the claimable for to true
+ vm.prank(owner);
+ instance.setClaimableFor(true);
- vm.expectRevert(InvalidMinThreshold.selector);
- hatsSignerGate.setMinThreshold(3);
+ // create the necessary arrays
+ address[] memory claimers = new address[](_signerCount);
+ uint256[] memory hatIds = new uint256[](_signerCount);
+ for (uint256 i; i < _signerCount; ++i) {
+ claimers[i] = signerAddresses[i];
+ hatIds[i] = signerHat;
}
- function testNonOwnerCannotSetMinThreshold() public {
- mockIsWearerCall(address(this), ownerHat, false);
+ // claim the signers, expecting the registered event to be emitted for each
+ for (uint256 i; i < _signerCount; ++i) {
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(signerHat, signerAddresses[i]);
+ }
+ instance.claimSignersFor(hatIds, claimers);
- vm.expectRevert("UNAUTHORIZED");
+ assertEq(instance.validSignerCount(), _signerCount, "incorrect valid signer count");
+ assertEq(safe.getOwners().length, _signerCount, "incorrect owner count");
+ }
- hatsSignerGate.setMinThreshold(1);
+ function test_startingWith1Signer_happy(uint256 _signerCount) public {
+ _signerCount = bound(_signerCount, 1, signerAddresses.length);
- assertEq(hatsSignerGate.minThreshold(), 2);
+ // set up signer validity
+ for (uint256 i; i < _signerCount; ++i) {
+ _setSignerValidity(signerAddresses[i], signerHat, true);
}
- function testReconcileSignerCount() public {
- mockIsWearerCall(addresses[1], signerHat, false);
- mockIsWearerCall(addresses[2], signerHat, false);
- mockIsWearerCall(addresses[3], signerHat, false);
- // add 3 more safe owners the old fashioned way
- // 1
- bytes memory addOwnersData1 = abi.encodeWithSignature("addOwnerWithThreshold(address,uint256)", addresses[1], 1);
-
- // mockIsWearerCall(address(this), signerHat, true);
- vm.prank(address(hatsSignerGate));
-
- safe.execTransactionFromModule(
- address(safe), // to
- 0, // value
- addOwnersData1, // data
- Enum.Operation.Call // operation
- );
-
- // 2
- bytes memory addOwnersData2 = abi.encodeWithSignature("addOwnerWithThreshold(address,uint256)", addresses[2], 1);
-
- // mockIsWearerCall(address(this), signerHat, true);
- vm.prank(address(hatsSignerGate));
-
- safe.execTransactionFromModule(
- address(safe), // to
- 0, // value
- addOwnersData2, // data
- Enum.Operation.Call // operation
- );
-
- // 3
- bytes memory addOwnersData3 = abi.encodeWithSignature("addOwnerWithThreshold(address,uint256)", addresses[3], 1);
-
- // mockIsWearerCall(address(this), signerHat, true);
- vm.prank(address(hatsSignerGate));
-
- safe.execTransactionFromModule(
- address(safe), // to
- 0, // value
- addOwnersData3, // data
- Enum.Operation.Call // operation
- );
-
- assertEq(hatsSignerGate.validSignerCount(), 0);
-
- // set only two of them as valid signers
- mockIsWearerCall(address(hatsSignerGate), signerHat, true);
- mockIsWearerCall(addresses[1], signerHat, true);
-
- // do the reconcile
- hatsSignerGate.reconcileSignerCount();
-
- assertEq(hatsSignerGate.validSignerCount(), 2);
- assertEq(safe.getThreshold(), 2);
-
- // now we can remove both the invalid signers with no changes to hatsSignerCount
- mockIsWearerCall(addresses[2], signerHat, false);
- hatsSignerGate.removeSigner(addresses[2]);
- mockIsWearerCall(addresses[3], signerHat, false);
- hatsSignerGate.removeSigner(addresses[3]);
-
- assertEq(hatsSignerGate.validSignerCount(), 2);
- assertEq(safe.getThreshold(), 2);
+ // set the claimable for to true
+ vm.prank(owner);
+ instance.setClaimableFor(true);
+
+ // add one signer to get rid of the placeholder owner
+ _addSignersSameHat(1, signerHat);
+ assertEq(instance.validSignerCount(), 1, "valid signer count should be 1");
+ assertEq(safe.getOwners().length, 1, "owner count should be 1");
+
+ // create the necessary arrays, starting with the next signer
+ address[] memory claimers = new address[](_signerCount - 1);
+ uint256[] memory hatIds = new uint256[](_signerCount - 1);
+ for (uint256 i; i < _signerCount - 1; ++i) {
+ claimers[i] = signerAddresses[i + 1];
+ hatIds[i] = signerHat;
}
- function testAddSingleSigner() public {
- addSigners(1);
-
- assertEq(safe.getOwners().length, 1);
-
- assertEq(hatsSignerGate.validSignerCount(), 1);
-
- assertEq(safe.getOwners()[0], addresses[0]);
-
- assertEq(safe.getThreshold(), 1);
+ // claim the signers, expecting the registered event to be emitted for each
+ for (uint256 i; i < _signerCount - 1; ++i) {
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(signerHat, signerAddresses[i + 1]);
}
+ instance.claimSignersFor(hatIds, claimers);
- function testAddThreeSigners() public {
- addSigners(3);
+ assertEq(instance.validSignerCount(), _signerCount, "incorrect valid signer count");
+ assertEq(safe.getOwners().length, _signerCount, "incorrect owner count");
+ }
- assertEq(hatsSignerGate.validSignerCount(), 3);
+ function test_alreadyOwnerNotRegistered_happy(uint256 _signerCount) public {
+ _signerCount = bound(_signerCount, 1, signerAddresses.length);
- assertEq(safe.getOwners()[0], addresses[2]);
- assertEq(safe.getOwners()[1], addresses[1]);
- assertEq(safe.getOwners()[2], addresses[0]);
-
- assertEq(safe.getThreshold(), 2);
+ // add _signerCount signers directly to the safe by pranking the safe
+ for (uint256 i; i < _signerCount; ++i) {
+ vm.prank(address(safe));
+ safe.addOwnerWithThreshold(signerAddresses[i], 1);
}
- function testAddTooManySigners() public {
- addSigners(5);
-
- mockIsWearerCall(addresses[5], signerHat, true);
-
- vm.expectRevert(MaxSignersReached.selector);
- vm.prank(addresses[5]);
-
- // this call should fail
- hatsSignerGate.claimSigner();
-
- assertEq(hatsSignerGate.validSignerCount(), 5);
-
- assertEq(safe.getOwners()[0], addresses[4]);
- assertEq(safe.getOwners()[1], addresses[3]);
- assertEq(safe.getOwners()[2], addresses[2]);
- assertEq(safe.getOwners()[3], addresses[1]);
- assertEq(safe.getOwners()[4], addresses[0]);
-
- assertEq(safe.getThreshold(), 2);
+ // set up signer validity
+ for (uint256 i; i < _signerCount; ++i) {
+ _setSignerValidity(signerAddresses[i], signerHat, true);
}
- function testClaimSigner() public {
- mockIsWearerCall(addresses[3], signerHat, true);
-
- vm.prank(addresses[3]);
- hatsSignerGate.claimSigner();
+ // set the claimable for to true
+ vm.prank(owner);
+ instance.setClaimableFor(true);
- assertEq(safe.getOwners()[0], addresses[3]);
- assertEq(safe.getThreshold(), 1);
- assertEq(safe.getOwners().length, 1);
+ // create the necessary arrays
+ address[] memory claimers = new address[](_signerCount);
+ uint256[] memory hatIds = new uint256[](_signerCount);
+ for (uint256 i; i < _signerCount; ++i) {
+ claimers[i] = signerAddresses[i];
+ hatIds[i] = signerHat;
}
- function testOwnerClaimSignerReverts() public {
- addSigners(2);
-
- vm.prank(addresses[1]);
-
- vm.expectRevert(abi.encodeWithSelector(SignerAlreadyClaimed.selector, addresses[1]));
-
- hatsSignerGate.claimSigner();
-
- assertEq(hatsSignerGate.validSignerCount(), 2);
+ // claim the signers, expecting the registered event to be emitted for each
+ for (uint256 i; i < _signerCount; ++i) {
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(signerHat, signerAddresses[i]);
}
+ instance.claimSignersFor(hatIds, claimers);
- function testNonHatWearerCannotClaimSigner() public {
- mockIsWearerCall(addresses[3], signerHat, false);
+ assertEq(instance.validSignerCount(), _signerCount, "incorrect valid signer count");
+ // owner count should be 1 more than the number of valid signers since the hsg instance is still an owner
+ assertEq(safe.getOwners().length, _signerCount + 1, "should be 1 more than the number of valid signers");
+ }
- vm.prank(addresses[3]);
+ function test_revert_notClaimableFor(uint256 _signerCount) public {
+ _signerCount = bound(_signerCount, 1, signerAddresses.length);
- vm.expectRevert(abi.encodeWithSelector(NotSignerHatWearer.selector, addresses[3]));
- hatsSignerGate.claimSigner();
+ // set up signer validity
+ for (uint256 i; i < _signerCount; ++i) {
+ _setSignerValidity(signerAddresses[i], signerHat, true);
}
- function testCanRemoveInvalidSigner1() public {
- addSigners(1);
-
- mockIsWearerCall(addresses[0], signerHat, false);
-
- hatsSignerGate.removeSigner(addresses[0]);
-
- assertEq(safe.getOwners().length, 1);
- assertEq(safe.getOwners()[0], address(hatsSignerGate));
- assertEq(hatsSignerGate.validSignerCount(), 0);
-
- assertEq(safe.getThreshold(), 1);
+ // set the claimable for to true and then undo it
+ vm.prank(owner);
+ instance.setClaimableFor(true);
+ vm.prank(owner);
+ instance.setClaimableFor(false);
+
+ // create the necessary arrays
+ address[] memory claimers = new address[](_signerCount);
+ uint256[] memory hatIds = new uint256[](_signerCount);
+ for (uint256 i; i < _signerCount; ++i) {
+ claimers[i] = signerAddresses[i];
+ hatIds[i] = signerHat;
}
- function testCanRemoveInvalidSignerWhenMultipleSigners() public {
- addSigners(2);
-
- mockIsWearerCall(addresses[0], signerHat, false);
-
- // emit log_uint(hatsSignerGate.signerCount());
+ vm.expectRevert(IHatsSignerGate.NotClaimableFor.selector);
+ instance.claimSignersFor(hatIds, claimers);
- hatsSignerGate.removeSigner(addresses[0]);
+ assertEq(instance.validSignerCount(), 0, "incorrect valid signer count");
+ assertEq(safe.getOwners().length, 1, "incorrect owner count");
+ }
- assertEq(safe.getOwners().length, 1);
- assertEq(safe.getOwners()[0], addresses[1]);
- assertEq(hatsSignerGate.validSignerCount(), 1);
+ function test_revert_invalidSignerHat(uint256 _signerCount) public {
+ _signerCount = bound(_signerCount, 1, signerAddresses.length);
+ uint256 invalidSignerHat = signerHat + 1;
- assertEq(safe.getThreshold(), 1);
+ // set up signer validity
+ for (uint256 i; i < _signerCount; ++i) {
+ _setSignerValidity(signerAddresses[i], signerHat, true);
}
- function testCanRemoveInvalidSignerAfterReconcile2Signers() public {
- addSigners(2);
+ // set the claimable for to true
+ vm.prank(owner);
+ instance.setClaimableFor(true);
- mockIsWearerCall(addresses[0], signerHat, false);
-
- hatsSignerGate.reconcileSignerCount();
- assertEq(hatsSignerGate.validSignerCount(), 1);
-
- hatsSignerGate.removeSigner(addresses[0]);
-
- assertEq(safe.getOwners().length, 1);
- assertEq(safe.getOwners()[0], addresses[1]);
- assertEq(hatsSignerGate.validSignerCount(), 1);
+ // create the necessary arrays
+ address[] memory claimers = new address[](_signerCount);
+ uint256[] memory hatIds = new uint256[](_signerCount);
+ for (uint256 i; i < _signerCount; ++i) {
+ claimers[i] = signerAddresses[i];
+ hatIds[i] = invalidSignerHat;
+ }
- assertEq(safe.getThreshold(), 1);
+ vm.expectRevert(abi.encodeWithSelector(IHatsSignerGate.InvalidSignerHat.selector, invalidSignerHat));
+ instance.claimSignersFor(hatIds, claimers);
+ }
+
+ function test_revert_invalidSigner(uint256 _signerCount, uint256 _invalidSignerIndex) public {
+ _signerCount = bound(_signerCount, 1, signerAddresses.length);
+ _invalidSignerIndex = bound(_invalidSignerIndex, 0, _signerCount - 1);
+
+ // set up signer validity
+ for (uint256 i; i < _signerCount; ++i) {
+ if (i == _invalidSignerIndex) {
+ _setSignerValidity(signerAddresses[i], signerHat, false);
+ } else {
+ _setSignerValidity(signerAddresses[i], signerHat, true);
+ }
}
- function testCanRemoveInvalidSignerAfterReconcile3PLusSigners() public {
- addSigners(3);
+ // set the claimable for to true
+ vm.prank(owner);
+ instance.setClaimableFor(true);
- mockIsWearerCall(addresses[0], signerHat, false);
+ // create the necessary arrays
+ address[] memory claimers = new address[](_signerCount);
+ uint256[] memory hatIds = new uint256[](_signerCount);
+ for (uint256 i; i < _signerCount; ++i) {
+ claimers[i] = signerAddresses[i];
+ hatIds[i] = signerHat;
+ }
- hatsSignerGate.reconcileSignerCount();
- assertEq(hatsSignerGate.validSignerCount(), 2);
+ vm.expectRevert(
+ abi.encodeWithSelector(IHatsSignerGate.NotSignerHatWearer.selector, signerAddresses[_invalidSignerIndex])
+ );
+ instance.claimSignersFor(hatIds, claimers);
+ }
+}
- hatsSignerGate.removeSigner(addresses[0]);
+/// @dev see HatsSignerGate.internals.t.sol:RemovingSignerInternals for tests of internal logic
+contract RemovingSigner is WithHSGInstanceTest {
+ function test_happy_removeSigner(uint256 _seed) public {
+ // get a random signer
+ address signer = _getRandomAddress(_seed);
+ // get a random valid signer hat
+ uint256 signerHat = _getRandomValidSignerHat(_seed);
+
+ // set the signer hat validity
+ _setSignerValidity(signer, signerHat, true);
+
+ // claim the signer
+ vm.prank(signer);
+ instance.claimSigner(signerHat);
+
+ // signer loses their hat
+ _setSignerValidity(signer, signerHat, false);
+
+ // remove the signer
+ instance.removeSigner(signer);
+
+ assertFalse(safe.isOwner(signer), "the signer should no longer be an owner");
+ assertFalse(instance.isValidSigner(signer), "the signer should no longer be a valid signer");
+ assertEq(instance.registeredSignerHats(signer), 0, "the signer should no longer be registered for any hats");
+ }
+
+ function test_revert_stillWearsSignerHat(uint256 _seed) public {
+ // get a random signer
+ address signer = _getRandomAddress(_seed);
+ // get a random valid signer hat
+ uint256 signerHat = _getRandomValidSignerHat(_seed);
+
+ // set the signer hat validity
+ _setSignerValidity(signer, signerHat, true);
+
+ // claim the signer
+ vm.prank(signer);
+ instance.claimSigner(signerHat);
+
+ // remove the signer should revert
+ vm.expectRevert(IHatsSignerGate.StillWearsSignerHat.selector);
+ instance.removeSigner(signer);
+
+ assertTrue(safe.isOwner(signer), "the signer should still be an owner");
+ assertTrue(instance.isValidSigner(signer), "the signer should still be a valid signer");
+ assertEq(instance.registeredSignerHats(signer), signerHat, "the signer should still be registered for their hat");
+ }
+}
- assertEq(safe.getOwners().length, 2);
- assertEq(safe.getOwners()[0], addresses[2]);
- assertEq(safe.getOwners()[1], addresses[1]);
- assertEq(hatsSignerGate.validSignerCount(), 2);
+contract Locking is WithHSGInstanceTest {
+ function test_happy_lock() public isLocked(false) callerIsOwner(true) {
+ vm.expectEmit();
+ emit IHatsSignerGate.HSGLocked();
+ vm.prank(caller);
+ instance.lock();
- assertEq(safe.getThreshold(), 2);
- }
+ assertEq(instance.locked(), true, "HSG should be locked");
+ }
- function testCannotRemoveValidSigner() public {
- addSigners(1);
+ function test_revert_locked(bool _callerIsOwner) public isLocked(true) callerIsOwner(_callerIsOwner) {
+ vm.expectRevert(IHatsSignerGate.Locked.selector);
+ vm.prank(caller);
+ instance.lock();
- mockIsWearerCall(addresses[0], signerHat, true);
+ assertEq(instance.locked(), true, "HSG should still be locked");
+ }
- vm.expectRevert(abi.encodeWithSelector(StillWearsSignerHat.selector, addresses[0]));
+ function test_revert_notOwner() public isLocked(false) callerIsOwner(false) {
+ vm.expectRevert(IHatsSignerGate.NotOwnerHatWearer.selector);
+ vm.prank(caller);
+ instance.lock();
- hatsSignerGate.removeSigner(addresses[0]);
+ assertEq(instance.locked(), false, "HSG should still be unlocked");
+ }
+}
- assertEq(safe.getOwners().length, 1);
- assertEq(safe.getOwners()[0], addresses[0]);
- assertEq(hatsSignerGate.validSignerCount(), 1);
+contract SettingOwnerHat is WithHSGInstanceTest {
+ function test_fuzz_happy_setOwnerHat(uint256 _newOwnerHat) public isLocked(false) callerIsOwner(true) {
+ vm.expectEmit();
+ emit IHatsSignerGate.OwnerHatSet(_newOwnerHat);
+ vm.prank(caller);
+ instance.setOwnerHat(_newOwnerHat);
- assertEq(safe.getThreshold(), 1);
- }
+ assertEq(instance.ownerHat(), _newOwnerHat, "owner hat should be new");
+ }
- function testExecTxByHatWearers() public {
- addSigners(3);
-
- uint256 preNonce = safe.nonce();
- uint256 preValue = 1 ether;
- uint256 transferValue = 0.2 ether;
- uint256 postValue = preValue - transferValue;
- address destAddress = addresses[3];
- // give the safe some eth
- hoax(address(safe), preValue);
-
- // create the tx
- bytes32 txHash = getTxHash(destAddress, transferValue, hex"00", safe);
-
- // have 3 signers sign it
- bytes memory signatures = createNSigsForTx(txHash, 3);
-
- // have one of the signers submit/exec the tx
- vm.prank(addresses[0]);
- safe.execTransaction(
- destAddress,
- transferValue,
- hex"00",
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
- // confirm it we executed by checking ETH balance changes
- assertEq(address(safe).balance, postValue);
- assertEq(destAddress.balance, transferValue);
- assertEq(safe.nonce(), preNonce + 1);
- // emit log_uint(address(safe).balance);
- }
+ function test_fuzz_revert_locked(uint256 _newOwnerHat, bool _callerIsOwner)
+ public
+ isLocked(true)
+ callerIsOwner(_callerIsOwner)
+ {
+ uint256 oldOwnerHat = instance.ownerHat();
- function testExecTxByNonHatWearersReverts() public {
- addSigners(3);
-
- uint256 preNonce = safe.nonce();
- uint256 preValue = 1 ether;
- uint256 transferValue = 0.2 ether;
- // uint256 postValue = preValue - transferValue;
- address destAddress = addresses[3];
- // give the safe some eth
- hoax(address(safe), preValue);
- // emit log_uint(address(safe).balance);
- // create tx to send some eth from safe to wherever
- // create the tx
- bytes32 txHash = getTxHash(destAddress, transferValue, hex"00", safe);
-
- // have 3 signers sign it
- bytes memory signatures = createNSigsForTx(txHash, 3);
-
- // removing the hats from 2 signers
- mockIsWearerCall(addresses[0], signerHat, false);
- mockIsWearerCall(addresses[1], signerHat, false);
-
- // emit log_uint(address(safe).balance);
- // have one of the signers submit/exec the tx
- vm.prank(addresses[0]);
-
- // vm.expectRevert(abi.encodeWithSelector(BelowMinThreshold.selector, minThreshold, 1));
- vm.expectRevert(InvalidSigners.selector);
-
- safe.execTransaction(
- destAddress,
- transferValue,
- hex"00",
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
-
- // confirm it was not executed by checking ETH balance changes
- assertEq(destAddress.balance, 0);
- assertEq(safe.nonce(), preNonce);
- }
+ vm.expectRevert(IHatsSignerGate.Locked.selector);
+ vm.prank(caller);
+ instance.setOwnerHat(_newOwnerHat);
- function testExecTxByTooFewOwnersReverts() public {
- // add a legit signer
- addSigners(1);
-
- // set up test values
- uint256 preNonce = safe.nonce();
- uint256 preValue = 1 ether;
- uint256 transferValue = 0.2 ether;
- // uint256 postValue = preValue - transferValue;
- address destAddress = addresses[3];
- // give the safe some eth
- hoax(address(safe), preValue);
-
- // have the remaining signer sign it
- // create the tx
- bytes32 txHash = getTxHash(destAddress, transferValue, hex"00", safe);
-
- // have them sign it
- bytes memory signatures = createNSigsForTx(txHash, 1);
-
- // have the legit signer exec the tx
- vm.prank(addresses[0]);
-
- mockIsWearerCall(addresses[0], signerHat, true);
-
- vm.expectRevert(
- abi.encodeWithSelector(BelowMinThreshold.selector, hatsSignerGate.minThreshold(), safe.getOwners().length)
- );
-
- safe.execTransaction(
- destAddress,
- transferValue,
- hex"00",
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
-
- // confirm it was not executed by checking ETH balance changes
- assertEq(destAddress.balance, 0);
- assertEq(safe.nonce(), preNonce);
- emit log_uint(address(safe).balance);
- }
+ assertEq(instance.ownerHat(), oldOwnerHat, "owner hat should be old");
+ }
- function testExecByLessThanMinThresholdReverts() public {
- addSigners(2);
-
- mockIsWearerCall(addresses[1], signerHat, false);
- assertEq(safe.getThreshold(), 2);
-
- // set up test values
- // uint256 preNonce = safe.nonce();
- uint256 preValue = 1 ether;
- uint256 transferValue = 0.2 ether;
- // uint256 postValue = preValue - transferValue;
- address destAddress = addresses[3];
- // give the safe some eth
- hoax(address(safe), preValue);
-
- // have the remaining signer sign it
- // create the tx
- bytes32 txHash = getTxHash(destAddress, transferValue, hex"00", safe);
- // have them sign it
- bytes memory signatures = createNSigsForTx(txHash, 1);
-
- hatsSignerGate.reconcileSignerCount();
- assertEq(safe.getThreshold(), 1);
-
- // vm.expectRevert(abi.encodeWithSelector(BelowMinThreshold.selector, minThreshold, 1));
- vm.expectRevert(InvalidSigners.selector);
- safe.execTransaction(
- destAddress,
- transferValue,
- hex"00",
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
- }
+ function test_fuzz_revert_notOwner(uint256 _newOwnerHat) public isLocked(false) callerIsOwner(false) {
+ uint256 oldOwnerHat = instance.ownerHat();
- function testCannotDisableModule() public {
- bytes memory disableModuleData =
- abi.encodeWithSignature("disableModule(address,address)", SENTINELS, address(hatsSignerGate));
+ vm.expectRevert(IHatsSignerGate.NotOwnerHatWearer.selector);
+ vm.prank(caller);
+ instance.setOwnerHat(_newOwnerHat);
- addSigners(2);
+ assertEq(instance.ownerHat(), oldOwnerHat, "owner hat should be old");
+ }
+}
- bytes32 txHash = getTxHash(address(safe), 0, disableModuleData, safe);
+contract AddingSignerHats is WithHSGInstanceTest {
+ function test_fuzz_happy_addSignerHats(uint8 _numHats) public isLocked(false) callerIsOwner(true) {
+ uint256[] memory signerHats = _getRandomSignerHats(_numHats);
+
+ vm.expectEmit();
+ emit IHatsSignerGate.SignerHatsAdded(signerHats);
+ vm.prank(caller);
+ instance.addSignerHats(signerHats);
+
+ assertValidSignerHats(instance, signerHats);
+ }
+
+ function test_fuzz_revert_locked(uint8 _numHats, bool _callerIsOwner)
+ public
+ isLocked(true)
+ callerIsOwner(_callerIsOwner)
+ {
+ uint256[] memory signerHats = _getRandomSignerHats(_numHats);
+
+ vm.expectRevert(IHatsSignerGate.Locked.selector);
+ vm.prank(caller);
+ instance.addSignerHats(signerHats);
+ }
+
+ function test_fuzz_revert_notOwner(uint8 _numHats) public isLocked(false) callerIsOwner(false) {
+ uint256[] memory signerHats = _getRandomSignerHats(_numHats);
+
+ vm.expectRevert(IHatsSignerGate.NotOwnerHatWearer.selector);
+ vm.prank(caller);
+ instance.addSignerHats(signerHats);
+ }
+}
- bytes memory signatures = createNSigsForTx(txHash, 2);
+/// @dev see HatsSignerGate.internals.t.sol:OwnerSettingsInternals for threshold config validation tests
+contract SettingThresholdConfig is WithHSGInstanceTest {
+ function test_fuzz_happy_setThresholdConfig(uint8 _type, uint8 _min, uint16 _target)
+ public
+ isLocked(false)
+ callerIsOwner(true)
+ {
+ // create a valid threshold config
+ IHatsSignerGate.TargetThresholdType thresholdType = IHatsSignerGate.TargetThresholdType(bound(_type, 0, 1));
+ IHatsSignerGate.ThresholdConfig memory config = _createValidThresholdConfig(thresholdType, _min, _target);
+
+ vm.expectEmit();
+ emit IHatsSignerGate.ThresholdConfigSet(config);
+ vm.prank(caller);
+ instance.setThresholdConfig(config);
+ }
+
+ function test_fuzz_revert_locked(uint8 _type, uint8 _min, uint16 _target, bool _callerIsOwner)
+ public
+ isLocked(true)
+ callerIsOwner(_callerIsOwner)
+ {
+ // create a valid threshold config
+ IHatsSignerGate.TargetThresholdType thresholdType = IHatsSignerGate.TargetThresholdType(bound(_type, 0, 1));
+ IHatsSignerGate.ThresholdConfig memory config = _createValidThresholdConfig(thresholdType, _min, _target);
+
+ vm.expectRevert(IHatsSignerGate.Locked.selector);
+ vm.prank(caller);
+ instance.setThresholdConfig(config);
+ }
+
+ function test_fuzz_revert_notOwner(uint8 _type, uint8 _min, uint16 _target)
+ public
+ isLocked(false)
+ callerIsOwner(false)
+ {
+ // create a valid threshold config
+ IHatsSignerGate.TargetThresholdType thresholdType = IHatsSignerGate.TargetThresholdType(bound(_type, 0, 1));
+ IHatsSignerGate.ThresholdConfig memory config = _createValidThresholdConfig(thresholdType, _min, _target);
+
+ vm.expectRevert(IHatsSignerGate.NotOwnerHatWearer.selector);
+ vm.prank(caller);
+ instance.setThresholdConfig(config);
+ }
+}
- mockIsWearerCall(addresses[0], signerHat, true);
- mockIsWearerCall(addresses[1], signerHat, true);
+contract SettingClaimableFor is WithHSGInstanceTest {
+ function test_fuzz_happy_setClaimableFor(bool _claimableFor) public isLocked(false) callerIsOwner(true) {
+ vm.expectEmit();
+ emit IHatsSignerGate.ClaimableForSet(_claimableFor);
+ vm.prank(caller);
+ instance.setClaimableFor(_claimableFor);
- vm.expectRevert(SignersCannotChangeModules.selector);
+ assertEq(instance.claimableFor(), _claimableFor, "claimableFor should be new");
+ }
- // execute tx
- safe.execTransaction(
- address(safe),
- 0,
- disableModuleData,
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
+ function test_fuzz_revert_locked(bool _claimableFor, bool _callerIsOwner)
+ public
+ isLocked(true)
+ callerIsOwner(_callerIsOwner)
+ {
+ bool oldClaimableFor = instance.claimableFor();
- // executeSafeTxFrom(address(this), disableModuleData, safe);
- }
+ vm.expectRevert(IHatsSignerGate.Locked.selector);
+ vm.prank(caller);
+ instance.setClaimableFor(_claimableFor);
- function testCannotDisableGuard() public {
- bytes memory disableGuardData = abi.encodeWithSignature("setGuard(address)", address(0x0));
+ assertEq(instance.claimableFor(), oldClaimableFor, "claimableFor should be old");
+ }
- addSigners(2);
+ function test_fuzz_revert_notOwner(bool _claimableFor) public isLocked(false) callerIsOwner(false) {
+ bool oldClaimableFor = instance.claimableFor();
- bytes32 txHash = getTxHash(address(safe), 0, disableGuardData, safe);
+ vm.expectRevert(IHatsSignerGate.NotOwnerHatWearer.selector);
+ vm.prank(caller);
+ instance.setClaimableFor(_claimableFor);
- bytes memory signatures = createNSigsForTx(txHash, 2);
+ assertEq(instance.claimableFor(), oldClaimableFor, "claimableFor should be old");
+ }
+}
- mockIsWearerCall(addresses[0], signerHat, true);
- mockIsWearerCall(addresses[1], signerHat, true);
+contract DetachingHSG is WithHSGInstanceTest {
+ function test_happy_detachHSG() public isLocked(false) callerIsOwner(true) {
+ vm.expectEmit();
+ emit IHatsSignerGate.Detached();
+ vm.prank(caller);
+ instance.detachHSG();
+
+ assertFalse(safe.isModuleEnabled(address(instance)), "HSG should not be a module");
+ assertEq(_getSafeGuard(address(safe)), address(0), "HSG should not be a guard");
+ }
+
+ function test_revert_locked(bool _callerIsOwner) public isLocked(true) callerIsOwner(_callerIsOwner) {
+ vm.expectRevert(IHatsSignerGate.Locked.selector);
+ vm.prank(caller);
+ instance.detachHSG();
+
+ assertTrue(safe.isModuleEnabled(address(instance)), "HSG should still be a module");
+ assertEq(_getSafeGuard(address(safe)), (address(instance)), "HSG should still be a guard");
+ }
+
+ function test_revert_notOwner() public isLocked(false) callerIsOwner(false) {
+ vm.expectRevert(IHatsSignerGate.NotOwnerHatWearer.selector);
+ vm.prank(caller);
+ instance.detachHSG();
+
+ assertTrue(safe.isModuleEnabled(address(instance)), "HSG should still be a module");
+ assertEq(_getSafeGuard(address(safe)), (address(instance)), "HSG should still be a guard");
+ }
+}
- vm.expectRevert(abi.encodeWithSelector(CannotDisableThisGuard.selector, address(hatsSignerGate)));
- safe.execTransaction(
- address(safe),
- 0,
- disableGuardData,
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
+contract MigratingToNewHSG is WithHSGInstanceTest {
+ HatsSignerGate newHSG;
+
+ function setUp() public override {
+ super.setUp();
+
+ // create the instance deployer
+ DeployInstance instanceDeployer = new DeployInstance();
+
+ // set up the deployment with the same parameters as the existing HSG (except for the nonce)
+ instanceDeployer.prepare1(
+ address(implementationHSG),
+ ownerHat,
+ signerHats,
+ thresholdConfig,
+ address(safe),
+ false,
+ false,
+ address(0), // no guard
+ new address[](0) // no modules
+ );
+ instanceDeployer.prepare2(true, 1);
+
+ // deploy the instance
+ newHSG = instanceDeployer.run();
+ }
+
+ function test_happy_noSignersToMigrate() public isLocked(false) callerIsOwner(true) {
+ vm.expectEmit();
+ emit IHatsSignerGate.Migrated(address(newHSG));
+ vm.prank(caller);
+ instance.migrateToNewHSG(address(newHSG), new uint256[](0), new address[](0));
+
+ assertEq(_getSafeGuard(address(safe)), address(newHSG), "guard should be the new HSG");
+ assertFalse(safe.isModuleEnabled(address(instance)), "old HSG should be disabled as module");
+ assertTrue(safe.isModuleEnabled(address(newHSG)), "new HSG should be enabled as module");
+ }
+
+ function test_happy_signersToMigrate(uint256 _count) public isLocked(false) callerIsOwner(true) {
+ uint256 count = bound(_count, 1, signerAddresses.length);
+ // add some signers to the existing HSG
+ _addSignersSameHat(count, signerHat);
+
+ // set the claimable for to true for the new HSG
+ vm.prank(owner);
+ newHSG.setClaimableFor(true);
+
+ // create the migration arrays
+ uint256[] memory hatIdsToMigrate = new uint256[](count);
+ address[] memory signersToMigrate = new address[](count);
+ for (uint256 i; i < count; ++i) {
+ hatIdsToMigrate[i] = signerHat;
+ signersToMigrate[i] = signerAddresses[i];
}
- function testCannotIncreaseThreshold() public {
- addSigners(3);
+ vm.expectEmit();
+ emit IHatsSignerGate.Migrated(address(newHSG));
+ vm.prank(caller);
+ instance.migrateToNewHSG(address(newHSG), hatIdsToMigrate, signersToMigrate);
- uint256 oldThreshold = safe.getThreshold();
- assertEq(oldThreshold, 2);
+ assertEq(_getSafeGuard(address(safe)), address(newHSG), "guard should be the new HSG");
+ assertFalse(safe.isModuleEnabled(address(instance)), "old HSG should be disabled as module");
+ assertTrue(safe.isModuleEnabled(address(newHSG)), "new HSG should be enabled as module");
- // data to increase the threshold data by 1
- bytes memory changeThresholdData = abi.encodeWithSignature("changeThreshold(uint256)", oldThreshold + 1);
-
- bytes32 txHash = getTxHash(address(safe), 0, changeThresholdData, safe);
+ // check that the signers are now in the new HSG
+ for (uint256 i; i < count; ++i) {
+ assertTrue(newHSG.isValidSigner(signersToMigrate[i]), "signer should be in the new HSG");
+ }
+ assertEq(newHSG.validSignerCount(), count, "valid signer count should be correct");
+ }
+
+ function test_revert_notClaimableFor_signersToMigrate(uint256 _count) public isLocked(false) callerIsOwner(true) {
+ uint256 count = bound(_count, 1, signerAddresses.length);
+ // add some signers to the existing HSG
+ _addSignersSameHat(count, signerHat);
+
+ // don't set the claimable for to true for the new HSG
+
+ // create the migration arrays
+ uint256[] memory hatIdsToMigrate = new uint256[](count);
+ address[] memory signersToMigrate = new address[](count);
+ for (uint256 i; i < count; ++i) {
+ hatIdsToMigrate[i] = signerHat;
+ signersToMigrate[i] = signerAddresses[i];
+ }
- bytes memory signatures = createNSigsForTx(txHash, 2);
+ vm.expectRevert(IHatsSignerGate.NotClaimableFor.selector);
+ vm.prank(caller);
+ instance.migrateToNewHSG(address(newHSG), hatIdsToMigrate, signersToMigrate);
- mockIsWearerCall(addresses[0], signerHat, true);
- mockIsWearerCall(addresses[1], signerHat, true);
+ assertEq(_getSafeGuard(address(safe)), address(instance), "guard should be the old HSG");
+ assertTrue(safe.isModuleEnabled(address(instance)), "old HSG should be enabled as module");
+ assertFalse(safe.isModuleEnabled(address(newHSG)), "new HSG should not be enabled as module");
- vm.expectRevert(abi.encodeWithSelector(SignersCannotChangeThreshold.selector));
- safe.execTransaction(
- address(safe),
- 0,
- changeThresholdData,
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
+ // check that the signers are now in the new HSG
+ for (uint256 i; i < count; ++i) {
+ assertFalse(newHSG.isValidSigner(signersToMigrate[i]), "signer should not be in the new HSG");
}
+ assertEq(newHSG.validSignerCount(), 0, "valid signer count should be 0");
+ }
+
+ function test_revert_nonOwner() public isLocked(false) callerIsOwner(false) {
+ vm.expectRevert(IHatsSignerGate.NotOwnerHatWearer.selector);
+ vm.prank(caller);
+ instance.migrateToNewHSG(address(newHSG), new uint256[](0), new address[](0));
+
+ assertEq(_getSafeGuard(address(safe)), address(instance), "guard should be the old HSG");
+ assertTrue(safe.isModuleEnabled(address(instance)), "old HSG should be enabled as module");
+ assertFalse(safe.isModuleEnabled(address(newHSG)), "new HSG should not be enabled as module");
+ }
+
+ function test_revert_locked() public isLocked(true) callerIsOwner(true) {
+ vm.expectRevert(IHatsSignerGate.Locked.selector);
+ vm.prank(caller);
+ instance.migrateToNewHSG(address(newHSG), new uint256[](0), new address[](0));
+
+ assertEq(_getSafeGuard(address(safe)), address(instance), "guard should be the old HSG");
+ assertTrue(safe.isModuleEnabled(address(instance)), "old HSG should be enabled as module");
+ assertFalse(safe.isModuleEnabled(address(newHSG)), "new HSG should not be enabled as module");
+ }
+}
- function testCannotDecreaseThreshold() public {
- addSigners(3);
+contract EnablingDelegatecallTarget is WithHSGInstanceTest {
+ function test_fuzz_happy_enableDelegatecallTarget(uint256 _seed) public isLocked(false) callerIsOwner(true) {
+ address target = _getRandomAddress(_seed);
- uint256 oldThreshold = safe.getThreshold();
- assertEq(oldThreshold, 2);
+ vm.expectEmit();
+ emit IHatsSignerGate.DelegatecallTargetEnabled(target, true);
+ vm.prank(caller);
+ instance.enableDelegatecallTarget(target);
- // data to decrease the threshold data by 1
- bytes memory changeThresholdData = abi.encodeWithSignature("changeThreshold(uint256)", oldThreshold - 1);
+ assertTrue(instance.enabledDelegatecallTargets(target), "new target should be enabled");
+ }
- bytes32 txHash = getTxHash(address(safe), 0, changeThresholdData, safe);
+ function test_fuzz_revert_locked(uint256 _seed, bool _callerIsOwner)
+ public
+ isLocked(true)
+ callerIsOwner(_callerIsOwner)
+ {
+ address target = _getRandomAddress(_seed);
- bytes memory signatures = createNSigsForTx(txHash, 2);
+ bool wasEnabled = instance.enabledDelegatecallTargets(target);
- mockIsWearerCall(addresses[0], signerHat, true);
- mockIsWearerCall(addresses[1], signerHat, true);
+ vm.expectRevert(IHatsSignerGate.Locked.selector);
+ vm.prank(caller);
+ instance.enableDelegatecallTarget(target);
- vm.expectRevert(abi.encodeWithSelector(SignersCannotChangeThreshold.selector));
- safe.execTransaction(
- address(safe),
- 0,
- changeThresholdData,
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
- }
+ assertEq(instance.enabledDelegatecallTargets(target), wasEnabled, "target enabled state should not change");
+ }
- function testSignersCannotAddOwners() public {
- addSigners(3);
- // data for call to add owners
- bytes memory addOwnerData = abi.encodeWithSignature(
- "addOwnerWithThreshold(address,uint256)",
- addresses[9], // newOwner
- safe.getThreshold() // threshold
- );
-
- bytes32 txHash = getTxHash(address(safe), 0, addOwnerData, safe);
- bytes memory signatures = createNSigsForTx(txHash, 2);
-
- // ensure 2 signers are valid
- mockIsWearerCall(addresses[0], signerHat, true);
- mockIsWearerCall(addresses[1], signerHat, true);
- // mock call to attempted new owner (doesn't matter if valid or not)
- mockIsWearerCall(addresses[9], signerHat, false);
-
- vm.expectRevert(abi.encodeWithSelector(SignersCannotChangeOwners.selector));
- safe.execTransaction(
- address(safe),
- 0,
- addOwnerData,
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
- }
+ function test_fuzz_revert_notOwner(uint256 _seed) public isLocked(false) callerIsOwner(false) {
+ address target = _getRandomAddress(_seed);
- function testSignersCannotRemoveOwners() public {
- addSigners(3);
- address toRemove = addresses[2];
- // data for call to remove owners
- bytes memory removeOwnerData = abi.encodeWithSignature(
- "removeOwner(address,address,uint256)",
- findPrevOwner(safe.getOwners(), toRemove), // prevOwner
- toRemove, // owner to remove
- safe.getThreshold() // threshold
- );
-
- bytes32 txHash = getTxHash(address(safe), 0, removeOwnerData, safe);
- bytes memory signatures = createNSigsForTx(txHash, 2);
-
- // ensure 2 signers are valid
- mockIsWearerCall(addresses[0], signerHat, true);
- mockIsWearerCall(addresses[1], signerHat, true);
-
- vm.expectRevert(abi.encodeWithSelector(SignersCannotChangeOwners.selector));
- safe.execTransaction(
- address(safe),
- 0,
- removeOwnerData,
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
- }
+ bool wasEnabled = instance.enabledDelegatecallTargets(target);
- function testSignersCannotSwapOwners() public {
- addSigners(3);
- address toRemove = addresses[2];
- address toAdd = addresses[9];
- // data for call to swap owners
- bytes memory swapOwnerData = abi.encodeWithSignature(
- "swapOwner(address,address,address)",
- findPrevOwner(safe.getOwners(), toRemove), // prevOwner
- toRemove, // owner to swap
- toAdd // newOwner
- );
-
- bytes32 txHash = getTxHash(address(safe), 0, swapOwnerData, safe);
- bytes memory signatures = createNSigsForTx(txHash, 2);
-
- // ensure 2 signers are valid
- mockIsWearerCall(addresses[0], signerHat, true);
- mockIsWearerCall(addresses[1], signerHat, true);
- // mock call to attempted new owner (doesn't matter if valid or not)
- mockIsWearerCall(toAdd, signerHat, false);
-
- vm.expectRevert(abi.encodeWithSelector(SignersCannotChangeOwners.selector));
- safe.execTransaction(
- address(safe),
- 0,
- swapOwnerData,
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
- }
+ vm.expectRevert(IHatsSignerGate.NotOwnerHatWearer.selector);
+ vm.prank(caller);
+ instance.enableDelegatecallTarget(target);
- function testCannotCallCheckTransactionFromNonSafe() public {
- vm.expectRevert(NotCalledFromSafe.selector);
- hatsSignerGate.checkTransaction(
- address(0), 0, hex"00", Enum.Operation.Call, 0, 0, 0, address(0), payable(address(0)), hex"00", address(0)
- );
- }
+ assertEq(instance.enabledDelegatecallTargets(target), wasEnabled, "target enabled state should not change");
+ }
+}
- function testCannotCallCheckAfterExecutionFromNonSafe() public {
- vm.expectRevert(NotCalledFromSafe.selector);
- hatsSignerGate.checkAfterExecution(hex"00", true);
- }
+contract DisablingDelegatecallTarget is WithHSGInstanceTest {
+ function test_fuzz_happy_disableDelegatecallTarget(uint256 _seed) public isLocked(false) callerIsOwner(true) {
+ address target = _getRandomAddress(_seed);
+
+ // enable the target first
+ vm.prank(owner);
+ instance.enableDelegatecallTarget(target);
+
+ // expect the target to be disabled
+ vm.expectEmit();
+ emit IHatsSignerGate.DelegatecallTargetEnabled(target, false);
+ vm.prank(caller);
+ instance.disableDelegatecallTarget(target);
+
+ assertFalse(instance.enabledDelegatecallTargets(target), "target should be disabled");
+ }
+
+ function test_revert_locked(uint256 _seed, bool _callerIsOwner) public isLocked(false) callerIsOwner(_callerIsOwner) {
+ address target = _getRandomAddress(_seed);
+
+ // enable the target first and then lock the HSG
+ vm.startPrank(owner);
+ instance.enableDelegatecallTarget(target);
+ instance.lock();
+ vm.stopPrank();
+ // expect the target to be disabled
+ vm.expectRevert(IHatsSignerGate.Locked.selector);
+ vm.prank(caller);
+ instance.disableDelegatecallTarget(target);
+
+ assertTrue(instance.enabledDelegatecallTargets(target), "target should still be enabled");
+ }
+
+ function test_revert_notOwner(uint256 _seed) public isLocked(false) callerIsOwner(false) {
+ address target = _getRandomAddress(_seed);
+
+ // enable the target first
+ vm.prank(owner);
+ instance.enableDelegatecallTarget(target);
+
+ // expect the target to be disabled
+ vm.expectRevert(IHatsSignerGate.NotOwnerHatWearer.selector);
+ vm.prank(caller);
+ instance.disableDelegatecallTarget(target);
+
+ assertTrue(instance.enabledDelegatecallTargets(target), "target should still be enabled");
+ }
+}
- function testAttackOnMaxSignerFails() public {
- // max signers is 5
- // 5 signers claim
- addSigners(5);
-
- // a signer misbehaves and loses the hat
- mockIsWearerCall(addresses[4], signerHat, false);
-
- // reconcile is called, so signerCount is updated to 4
- hatsSignerGate.reconcileSignerCount();
- assertEq(hatsSignerGate.validSignerCount(), 4);
-
- // a new signer claims, so signerCount is updated to 5
- mockIsWearerCall(addresses[5], signerHat, true);
- vm.prank(addresses[5]);
- hatsSignerGate.claimSigner();
- assertEq(hatsSignerGate.validSignerCount(), 5);
-
- // the malicious signer behaves nicely and regains the hat, but they were kicked out by the previous signer claim
- mockIsWearerCall(addresses[4], signerHat, true);
-
- // reoncile is called again and signerCount stays at 5
- // vm.expectRevert(MaxSignersReached.selector);
- hatsSignerGate.reconcileSignerCount();
- assertEq(hatsSignerGate.validSignerCount(), 5);
-
- // // any eligible signer can now claim at will
- // mockIsWearerCall(addresses[6], signerHat, true);
- // vm.prank(addresses[6]);
- // hatsSignerGate.claimSigner();
- // assertEq(hatsSignerGate.signerCount(), 7);
- }
+/// @dev Tests for internal logic of Modifier.enableModule function can be found here:
+/// https://github.com/gnosisguild/zodiac/blob/18b7575bb342424537883f7ebe0a94cd7f3ec4f6/test/03_Modifier.spec.ts
+contract EnablingModule is WithHSGInstanceTest {
+ function test_happy_enableModule(uint256 _seed) public isLocked(false) callerIsOwner(true) {
+ address module = _getRandomAddress(_seed);
- function testAttackOnMaxSigner2Fails() public {
- // max signers is x
- // 1) we grant x signers
- addSigners(5);
- // 2) 3 signers lose validity
- mockIsWearerCall(addresses[2], signerHat, false);
- mockIsWearerCall(addresses[3], signerHat, false);
- mockIsWearerCall(addresses[4], signerHat, false);
-
- // 3) reconcile is called, signerCount=x-3
- hatsSignerGate.reconcileSignerCount();
- console2.log("A");
- assertEq(hatsSignerGate.validSignerCount(), 2);
-
- // 4) 3 more signers can be added with claimSigner()
- mockIsWearerCall(addresses[5], signerHat, true);
- vm.prank(addresses[5]);
- hatsSignerGate.claimSigner();
- mockIsWearerCall(addresses[6], signerHat, true);
- vm.prank(addresses[6]);
- hatsSignerGate.claimSigner();
- mockIsWearerCall(addresses[7], signerHat, true);
- vm.prank(addresses[7]);
- hatsSignerGate.claimSigner();
-
- console2.log("B");
- assertEq(hatsSignerGate.validSignerCount(), 5);
- console2.log("C");
- assertEq(safe.getOwners().length, 5);
-
- // 5) the 3 signers from (2) regain their validity
- mockIsWearerCall(addresses[2], signerHat, true);
- mockIsWearerCall(addresses[3], signerHat, true);
- mockIsWearerCall(addresses[4], signerHat, true);
-
- // but we still only have 5 owners and 5 signers
- console2.log("D");
- assertEq(hatsSignerGate.validSignerCount(), 5);
-
- console2.log("E");
- assertEq(safe.getOwners().length, 5);
-
- console2.log("F");
- hatsSignerGate.reconcileSignerCount();
- assertEq(hatsSignerGate.validSignerCount(), 5);
-
- // // 6) we now have x+3 signers
- // hatsSignerGate.reconcileSignerCount();
- // assertEq(hatsSignerGate.signerCount(), 8);
- }
+ vm.expectEmit();
+ emit IAvatar.EnabledModule(module);
+ vm.prank(caller);
+ instance.enableModule(module);
- function testValidSignersCanClaimAfterMaxSignerLosesHat() public {
- // max signers is 5
- // 5 signers claim
- addSigners(5);
+ assertTrue(instance.isModuleEnabled(module), "module should be enabled");
+ }
- // a signer misbehaves and loses the hat
- mockIsWearerCall(addresses[4], signerHat, false);
+ function test_revert_locked(uint256 _seed, bool _callerIsOwner) public isLocked(true) callerIsOwner(_callerIsOwner) {
+ address module = _getRandomAddress(_seed);
- // reconcile is called, so signerCount is updated to 4
- hatsSignerGate.reconcileSignerCount();
+ vm.expectRevert(IHatsSignerGate.Locked.selector);
+ vm.prank(caller);
+ instance.enableModule(module);
- mockIsWearerCall(addresses[5], signerHat, true);
- vm.prank(addresses[5]);
- hatsSignerGate.claimSigner();
- }
+ assertFalse(instance.isModuleEnabled(module), "module should not be enabled");
+ }
- function testValidSignersCanClaimAfterLastMaxSignerLosesHat() public {
- // max signers is 5
- // 5 signers claim
- addSigners(5);
+ function test_fuzz_revert_notOwner(uint256 _seed) public isLocked(false) callerIsOwner(false) {
+ address module = _getRandomAddress(_seed);
- address[] memory owners = safe.getOwners();
+ vm.expectRevert(IHatsSignerGate.NotOwnerHatWearer.selector);
+ vm.prank(caller);
+ instance.enableModule(module);
- // a signer misbehaves and loses the hat
- mockIsWearerCall(owners[4], signerHat, false);
+ assertFalse(instance.isModuleEnabled(module), "module should not be enabled");
+ }
+}
- // validSignerCount is now 4
- assertEq(hatsSignerGate.validSignerCount(), 4);
+/// @dev Tests for internal logic of Modifier.disableModule function can be found here:
+/// https://github.com/gnosisguild/zodiac/blob/18b7575bb342424537883f7ebe0a94cd7f3ec4f6/test/03_Modifier.spec.ts
+contract DisablingModule is WithHSGInstanceTest {
+ function test_happy_disableModule(uint256 _seed) public isLocked(false) callerIsOwner(true) {
+ address module = _getRandomAddress(_seed);
+
+ // enable the module first
+ vm.prank(owner);
+ instance.enableModule(module);
+
+ // expect the module to be disabled
+ vm.expectEmit();
+ emit IAvatar.DisabledModule(module);
+ vm.prank(caller);
+ instance.disableModule({ prevModule: SENTINELS, module: module });
+
+ assertFalse(instance.isModuleEnabled(module), "module should be disabled");
+ }
+
+ function test_happy_disableModule_twoModules(uint256 _seed) public isLocked(false) callerIsOwner(true) {
+ address module1 = _getRandomAddress(_seed);
+ address module2 = address(uint160(uint256(keccak256(abi.encode(_getRandomAddress(_seed))))));
+
+ // enable both modules
+ vm.startPrank(owner);
+ instance.enableModule(module1);
+ instance.enableModule(module2);
+ vm.stopPrank();
+
+ // disable the first module
+ vm.expectEmit();
+ emit IAvatar.DisabledModule(module1);
+ vm.prank(caller);
+ instance.disableModule({ prevModule: module2, module: module1 });
+
+ assertFalse(instance.isModuleEnabled(module1), "module1 should be disabled");
+ }
+
+ function test_revert_locked(uint256 _seed, bool _callerIsOwner) public isLocked(false) callerIsOwner(_callerIsOwner) {
+ address module = _getRandomAddress(_seed);
+
+ // enable the module first, then lock the HSG
+ vm.startPrank(owner);
+ instance.enableModule(module);
+ instance.lock();
+ vm.stopPrank();
+
+ // expect the module to not be disabled
+ vm.expectRevert(IHatsSignerGate.Locked.selector);
+ vm.prank(caller);
+ instance.disableModule({ prevModule: SENTINELS, module: module });
+
+ assertTrue(instance.isModuleEnabled(module), "module should still be enabled");
+ }
+
+ function test_revert_notOwner(uint256 _seed) public isLocked(false) callerIsOwner(false) {
+ address module = _getRandomAddress(_seed);
+
+ // enable the module first
+ vm.prank(owner);
+ instance.enableModule(module);
+
+ // expect the module to not be disabled
+ vm.expectRevert(IHatsSignerGate.NotOwnerHatWearer.selector);
+ vm.prank(caller);
+ instance.disableModule({ prevModule: SENTINELS, module: module });
+
+ assertTrue(instance.isModuleEnabled(module), "module should still be enabled");
+ }
+}
- mockIsWearerCall(addresses[5], signerHat, true);
- vm.prank(addresses[5]);
- hatsSignerGate.claimSigner();
- }
+/// @dev Tests for internal logic of Guardable.setGuard function can be found here:
+/// https://github.com/gnosisguild/zodiac/blob/18b7575bb342424537883f7ebe0a94cd7f3ec4f6/test/04_Guardable.spec.ts
+contract SettingGuard is WithHSGInstanceTest {
+ function test_happy_setGuard(uint256 _seed) public isLocked(false) callerIsOwner(true) {
+ // get a random guard
+ address newGuard = _getRandomGuard(_seed);
- function testSignersCannotAddNewModules() public {
- (address[] memory modules,) = safe.getModulesPaginated(SENTINELS, 5);
- console2.log(modules.length);
- // console2.log(modules[1]);
+ // expect the guard to be set
+ vm.expectEmit();
+ emit GuardableUnowned.ChangedGuard(newGuard);
+ vm.prank(caller);
+ instance.setGuard(newGuard);
- bytes memory addModuleData = abi.encodeWithSignature("enableModule(address)", address(0xf00baa)); // some devs are from Boston
+ assertEq(instance.getGuard(), newGuard, "guard should be new");
- addSigners(2);
+ // now remove the guard
+ vm.expectEmit();
+ emit GuardableUnowned.ChangedGuard(address(0));
+ vm.prank(caller);
+ instance.setGuard(address(0));
- bytes32 txHash = getTxHash(address(safe), 0, addModuleData, safe);
+ assertEq(instance.getGuard(), address(0), "guard should be removed");
+ }
- bytes memory signatures = createNSigsForTx(txHash, 2);
+ function test_revert_notIERC165Compliant(uint256 _seed) public isLocked(false) callerIsOwner(true) {
+ address newGuard = _getRandomAddress(_seed);
- mockIsWearerCall(addresses[0], signerHat, true);
- mockIsWearerCall(addresses[1], signerHat, true);
+ vm.expectRevert();
+ vm.prank(caller);
+ instance.setGuard(newGuard);
- vm.expectRevert(SignersCannotChangeModules.selector);
+ assertEq(instance.getGuard(), address(0), "guard should not be set");
+ }
- // execute tx
- safe.execTransaction(
- address(safe),
- 0,
- addModuleData,
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
- }
+ function test_revert_locked(uint256 _seed, bool _callerIsOwner) public isLocked(true) callerIsOwner(_callerIsOwner) {
+ address newGuard = _getRandomGuard(_seed);
- function testTargetSigAttackFails() public {
- // set target threshold to 5
- mockIsWearerCall(address(this), ownerHat, true);
- hatsSignerGate.setTargetThreshold(5);
- // initially there are 5 signers
- addSigners(5);
-
- // 3 owners lose their hats
- mockIsWearerCall(addresses[2], signerHat, false);
- mockIsWearerCall(addresses[3], signerHat, false);
- mockIsWearerCall(addresses[4], signerHat, false);
-
- // reconcile is called, so signerCount is updated to 2
- hatsSignerGate.reconcileSignerCount();
- assertEq(hatsSignerGate.validSignerCount(), 2);
- assertEq(safe.getThreshold(), 2);
-
- // the 3 owners regain their hats
- mockIsWearerCall(addresses[2], signerHat, true);
- mockIsWearerCall(addresses[3], signerHat, true);
- mockIsWearerCall(addresses[4], signerHat, true);
-
- // set up test values
- // uint256 preNonce = safe.nonce();
- uint256 preValue = 1 ether;
- uint256 transferValue = 0.2 ether;
- // uint256 postValue = preValue - transferValue;
- address destAddress = addresses[3];
- // give the safe some eth
- hoax(address(safe), preValue);
-
- // have just 2 of 5 signers sign it
- // create the tx
- bytes32 txHash = getTxHash(destAddress, transferValue, hex"00", safe);
- // have them sign it
- bytes memory signatures = createNSigsForTx(txHash, 2);
-
- vm.expectRevert();
- safe.execTransaction(
- destAddress,
- transferValue,
- hex"00",
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
- }
+ vm.expectRevert(IHatsSignerGate.Locked.selector);
+ vm.prank(caller);
+ instance.setGuard(newGuard);
- function testCannotClaimSignerIfNoInvalidSigners() public {
- assertEq(maxSigners, 5);
- addSigners(5);
- // one signer loses their hat
- mockIsWearerCall(addresses[4], signerHat, false);
- assertEq(hatsSignerGate.validSignerCount(), 4);
-
- // reconcile is called, updating signer count to 4
- hatsSignerGate.reconcileSignerCount();
- assertEq(hatsSignerGate.validSignerCount(), 4);
-
- // bad signer regains their hat
- mockIsWearerCall(addresses[4], signerHat, true);
- // signer count returns to 5
- assertEq(hatsSignerGate.validSignerCount(), 5);
-
- // new valid signer tries to claim, but can't because we're already at max signers
- mockIsWearerCall(addresses[5], signerHat, true);
- vm.prank(addresses[5]);
- vm.expectRevert(MaxSignersReached.selector);
- hatsSignerGate.claimSigner();
- }
+ assertEq(instance.getGuard(), address(0), "guard should not be set");
+ }
- function testRemoveSignerCorrectlyUpdates() public {
- assertEq(hatsSignerGate.targetThreshold(), 2, "target threshold");
- assertEq(maxSigners, 5, "max signers");
- // start with 5 valid signers
- addSigners(5);
+ function test_revert_notOwner(uint256 _seed) public isLocked(false) callerIsOwner(false) {
+ address newGuard = _getRandomGuard(_seed);
- // the last two lose their hats
- mockIsWearerCall(addresses[3], signerHat, false);
- mockIsWearerCall(addresses[4], signerHat, false);
+ vm.expectRevert(IHatsSignerGate.NotOwnerHatWearer.selector);
+ vm.prank(caller);
+ instance.setGuard(newGuard);
- // the 4th regains its hat
- mockIsWearerCall(addresses[3], signerHat, true);
+ assertEq(instance.getGuard(), address(0), "guard should not be set");
+ }
+}
- // remove the 5th signer
- hatsSignerGate.removeSigner(addresses[4]);
+/// @dev These tests use the harness to access the transient state variables set within checkTransaction. Most of the
+/// tests call harness.exposed_checkTransaction, which wraps instance.checkTransaction and stores the transient state in
+/// persistent storage for access in tests.
+contract CheckTransaction is WithHSGHarnessInstanceTest {
+ uint256 public simulatedInitialNonce;
+
+ function setUp() public override {
+ super.setUp();
+
+ // Execute an empty transaction from the safe to set the nonce to force the safe's nonce to increment. This is
+ // necessary to simulate the conditions under which HSG.checkTransaction is called in practice, ie just after the
+ // nonce has incremented. This also adds two signers.
+ _executeEmptyCallFromSafe(2, address(org));
+
+ assertGt(safe.nonce(), 0, "safe nonce should gt 0");
+
+ simulatedInitialNonce = safe.nonce() - 1;
+ }
+
+ function test_happy_checkTransaction_callToNonSafe(uint256 _seed)
+ public
+ callerIsSafe(true)
+ inSafeExecTransaction(false)
+ inModuleExecTransaction(false)
+ {
+ address target = _getRandomAddress(_seed);
+ vm.assume(target != address(safe));
+
+ // get the signatures for an empty delegatecall to the target
+ // we use contract signatures here to avoid dealing with txhash decoding, which requires the nonce to be correct,
+ // which its not since we're skipping the safe.execTransaction call that increments it
+ bytes memory signatures = _createNContractSigs(2);
+
+ vm.prank(caller);
+ harness.exposed_checkTransaction(
+ target, 0, new bytes(0), Enum.Operation.Call, 0, 0, 0, address(0), payable(0), signatures, address(0)
+ );
+
+ _assertTransientStateVariables({
+ _operation: Enum.Operation.Call, // this is a call
+ _existingOwnersHash: bytes32(0), // empty because call
+ _existingThreshold: 0, // empty because call
+ _existingFallbackHandler: address(0), // empty because call
+ _inSafeExecTransaction: true,
+ _inModuleExecTransaction: false, // not a module tx
+ _initialNonce: simulatedInitialNonce,
+ _checkTransactionCounter: 1
+ });
+ }
+
+ function test_revert_notCalledFromSafe()
+ public
+ callerIsSafe(false)
+ inSafeExecTransaction(false)
+ inModuleExecTransaction(false)
+ {
+ vm.expectRevert(IHatsSignerGate.NotCalledFromSafe.selector);
+ vm.prank(caller);
+ harness.exposed_checkTransaction(
+ address(0), 0, new bytes(0), Enum.Operation.Call, 0, 0, 0, address(0), payable(0), new bytes(0), address(0)
+ );
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_guardReverts()
+ public
+ callerIsSafe(true)
+ inSafeExecTransaction(false)
+ inModuleExecTransaction(false)
+ {
+ // add a test guard
+ vm.prank(owner);
+ harness.setGuard(address(tstGuard));
+
+ // mock the guard's checkTransaction to revert
+ vm.mockCallRevert(address(tstGuard), abi.encodeWithSelector(tstGuard.checkTransaction.selector), "");
+
+ // call to checkTransaction should revert
+ vm.expectRevert();
+ vm.prank(caller);
+ harness.exposed_checkTransaction(
+ address(0), 0, new bytes(0), Enum.Operation.Call, 0, 0, 0, address(0), payable(0), new bytes(0), address(0)
+ );
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_delegatecallTargetEnabled()
+ public
+ callerIsSafe(true)
+ inSafeExecTransaction(false)
+ inModuleExecTransaction(false)
+ {
+ // enable a delegatecall target
+ address target = defaultDelegatecallTargets[0];
+ vm.prank(owner);
+ harness.enableDelegatecallTarget(target);
+
+ // get the signatures for an empty delegatecall to the target
+ // we use contract signatures here to avoid dealing with txhash decoding, which requires the nonce to be correct,
+ // which its not since we're skipping the safe.execTransaction call that increments it
+ bytes memory signatures = _createNContractSigs(2);
+
+ uint256 expectedThreshold = safe.getThreshold();
+ address expectedFallbackHandler = SafeManagerLib.getSafeFallbackHandler(safe);
+ bytes32 expectedOwnersHash = keccak256(abi.encode(safe.getOwners()));
+
+ vm.prank(caller);
+ harness.exposed_checkTransaction(
+ target, 0, new bytes(0), Enum.Operation.DelegateCall, 0, 0, 0, address(0), payable(0), signatures, address(0)
+ );
+
+ // transient state should be populated
+ _assertTransientStateVariables({
+ _operation: Enum.Operation.DelegateCall, // delegatecall
+ _existingOwnersHash: expectedOwnersHash, // populated since delegatecall
+ _existingThreshold: expectedThreshold, // populated since delegatecall
+ _existingFallbackHandler: expectedFallbackHandler, // populated since delegatecall
+ _inSafeExecTransaction: true,
+ _inModuleExecTransaction: false, // not a module tx
+ _initialNonce: simulatedInitialNonce,
+ _checkTransactionCounter: 1
+ });
+ }
+
+ function test_revert_delegatecallTargetNotEnabled()
+ public
+ callerIsSafe(true)
+ inSafeExecTransaction(false)
+ inModuleExecTransaction(false)
+ {
+ // expect the checkTransaction to revert
+ vm.expectRevert(IHatsSignerGate.DelegatecallTargetNotEnabled.selector);
+ vm.prank(caller);
+ harness.exposed_checkTransaction(
+ address(0),
+ 0,
+ new bytes(0),
+ Enum.Operation.DelegateCall,
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(0),
+ new bytes(0),
+ address(0)
+ );
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_cannotCallSafe()
+ public
+ callerIsSafe(true)
+ inSafeExecTransaction(false)
+ inModuleExecTransaction(false)
+ {
+ vm.expectRevert(IHatsSignerGate.CannotCallSafe.selector);
+ vm.prank(caller);
+ harness.exposed_checkTransaction(
+ address(safe), 0, new bytes(0), Enum.Operation.Call, 0, 0, 0, address(0), payable(0), new bytes(0), address(0)
+ );
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_thresholdTooLow(uint8 _operation)
+ public
+ callerIsSafe(true)
+ inSafeExecTransaction(false)
+ inModuleExecTransaction(false)
+ {
+ // bound the arg
+ Enum.Operation operation = Enum.Operation(bound(_operation, 0, 1));
+
+ // remove one signer to make the threshold too low
+ _setSignerValidity(signerAddresses[0], signerHat, false);
+ vm.prank(owner);
+ harness.removeSigner(signerAddresses[0]);
+
+ address target;
+
+ // enable a delegatecall target if we're checking delegatecalls
+ if (operation == Enum.Operation.DelegateCall) {
+ target = defaultDelegatecallTargets[0];
+ vm.prank(owner);
+ harness.enableDelegatecallTarget(target);
+ } else {
+ target = _getRandomAddress();
+ }
- // signer count should be 4 and threshold at target
- assertEq(hatsSignerGate.validSignerCount(), 4, "valid signer count");
- assertEq(safe.getThreshold(), hatsSignerGate.targetThreshold(), "ending threshold");
+ // expect the checkTransaction to revert
+ vm.expectRevert(IHatsSignerGate.ThresholdTooLow.selector);
+ vm.prank(caller);
+ harness.exposed_checkTransaction(
+ target, 0, new bytes(0), operation, 0, 0, 0, address(0), payable(0), new bytes(0), address(0)
+ );
+
+ // transient state should be cleared
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_insufficientValidSignatures(uint8 _operation)
+ public
+ callerIsSafe(true)
+ inSafeExecTransaction(false)
+ inModuleExecTransaction(false)
+ {
+ // bound the arg
+ Enum.Operation operation = Enum.Operation(bound(_operation, 0, 1));
+
+ // invalidate one signer but don't remove them
+ _setSignerValidity(signerAddresses[0], signerHat, false);
+
+ address target;
+
+ // enable a delegatecall target if we're checking delegatecalls
+ if (operation == Enum.Operation.DelegateCall) {
+ target = defaultDelegatecallTargets[0];
+ vm.prank(owner);
+ harness.enableDelegatecallTarget(target);
+ } else {
+ target = _getRandomAddress();
}
- function testCanClaimToReplaceInvalidSignerAtMaxSigner() public {
- assertEq(maxSigners, 5, "max signers");
- // start with 5 valid signers (the max)
- addSigners(5);
+ // create two contract signatures
+ bytes memory signatures = _createNContractSigs(2);
+
+ // expect the checkTransaction to revert
+ vm.expectRevert(IHatsSignerGate.InsufficientValidSignatures.selector);
+ vm.prank(caller);
+ harness.exposed_checkTransaction(
+ target, 0, new bytes(0), operation, 0, 0, 0, address(0), payable(0), signatures, address(0)
+ );
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_inSafeExecTransaction(bool _inModuleExecTransaction)
+ public
+ callerIsSafe(true)
+ inSafeExecTransaction(true)
+ inModuleExecTransaction(_inModuleExecTransaction)
+ {
+ vm.expectRevert(IHatsSignerGate.NoReentryAllowed.selector);
+ vm.prank(caller);
+ harness.exposed_checkTransaction(
+ address(safe), 0, new bytes(0), Enum.Operation.Call, 0, 0, 0, address(0), payable(0), new bytes(0), address(0)
+ );
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_inModuleExecTransaction(bool _inSafeExecTransaction)
+ public
+ callerIsSafe(true)
+ inSafeExecTransaction(_inSafeExecTransaction)
+ inModuleExecTransaction(true)
+ {
+ vm.expectRevert(IHatsSignerGate.NoReentryAllowed.selector);
+ vm.prank(caller);
+ harness.exposed_checkTransaction(
+ address(safe), 0, new bytes(0), Enum.Operation.Call, 0, 0, 0, address(0), payable(0), new bytes(0), address(0)
+ );
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_noReentryAllowed()
+ public
+ callerIsSafe(true)
+ inSafeExecTransaction(false)
+ inModuleExecTransaction(false)
+ {
+ // first craft a dummy/empty tx to pass to checkTransaction
+ bytes32 dummyTxHash = safe.getTransactionHash(
+ address(this), // send 0 eth to this contract
+ 0,
+ hex"00",
+ Enum.Operation.Call,
+ 0,
+ 0,
+ 0,
+ address(0),
+ address(0),
+ safe.nonce()
+ );
+
+ bytes memory dummyTxSigs = _createNSigsForTx(dummyTxHash, 2);
+
+ // create the calldata for a call back to checkTransaction
+ bytes memory reentryCall = abi.encodeWithSelector(
+ HatsSignerGate.checkTransaction.selector,
+ address(0),
+ 0,
+ "",
+ Enum.Operation.Call,
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(0),
+ dummyTxSigs,
+ address(this)
+ );
+
+ // get the txHash for the reentry call
+ bytes32 txHash = safe.getTransactionHash(
+ address(harness), 0, reentryCall, Enum.Operation.Call, 0, 0, 0, address(0), payable(0), safe.nonce()
+ );
+
+ // create 2 valid signatures for the txHash
+ bytes memory signatures = _createNSigsForTx(txHash, 2);
+
+ // expect the checkTransaction to revert. HSG will throw IHatsSignerGate.NoReentryAllowed, but the Safe will catch
+ // it and re-throw "GS013"
+ vm.expectRevert("GS013");
+ safe.execTransaction(
+ address(harness), 0, reentryCall, Enum.Operation.Call, 0, 0, 0, address(0), payable(0), signatures
+ );
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+}
- // the last one loses their hat
- mockIsWearerCall(addresses[4], signerHat, false);
+/// @dev These tests use the harness to ensure that the transient state that would normally be set by checkTransaction
+/// is available in checkAfterExecution.
+/// Most of these tests call harness.exposed_checkAfterExecution to accomplish this.
+/// Additonally, these tests do not cover the internal Safe state checks. See
+/// HatsSignerGate.internals.t.sol:TransactionValidationInternals for comprehensive tests of that logic.
+contract CheckAfterExecution is WithHSGHarnessInstanceTest {
+ function test_happy_checkAfterExecution(bytes32 _txHash, bool _success)
+ public
+ inSafeExecTransaction(true)
+ inModuleExecTransaction(false)
+ {
+ // call to checkAfterExecution should not revert
+ harness.exposed_checkAfterExecution(_txHash, _success);
+
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_guardReverts(bytes32 _txHash, bool _success)
+ public
+ inSafeExecTransaction(true)
+ inModuleExecTransaction(false)
+ {
+ // add a test guard
+ vm.prank(owner);
+ harness.setGuard(address(tstGuard));
+
+ // mock the guard's checkTransaction to revert
+ vm.mockCallRevert(address(tstGuard), abi.encodeWithSelector(tstGuard.checkAfterExecution.selector), "");
+
+ // call to checkAfterExecution should revert
+ vm.expectRevert();
+ harness.exposed_checkAfterExecution(_txHash, _success);
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_notInSafeExecTransaction(bytes32 _txHash, bool _success)
+ public
+ inSafeExecTransaction(false)
+ inModuleExecTransaction(false)
+ {
+ // should revert because we are not inside a Safe execTransaction call
+ vm.expectRevert(IHatsSignerGate.NoReentryAllowed.selector);
+ harness.exposed_checkAfterExecution(_txHash, _success);
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+
+ function test_revert_inModuleExecTransaction(bytes32 _txHash, bool _success)
+ public
+ inSafeExecTransaction(true)
+ inModuleExecTransaction(true)
+ {
+ // should revert because we are not inside a Safe execTransaction call
+ vm.expectRevert(IHatsSignerGate.NoReentryAllowed.selector);
+ harness.exposed_checkAfterExecution(_txHash, _success);
+
+ // transient state should be cleared after revert
+ _assertTransientStateVariables({
+ _operation: Enum.Operation(uint8(0)),
+ _existingOwnersHash: bytes32(0),
+ _existingThreshold: 0,
+ _existingFallbackHandler: address(0),
+ _inSafeExecTransaction: false,
+ _inModuleExecTransaction: false,
+ _initialNonce: 0,
+ _checkTransactionCounter: 0
+ });
+ }
+}
- // a new signer valid tries to claim, and can
- mockIsWearerCall(addresses[5], signerHat, true);
- vm.prank(addresses[5]);
- hatsSignerGate.claimSigner();
- assertEq(hatsSignerGate.validSignerCount(), 5, "valid signer count");
- }
+contract Views is WithHSGInstanceTest {
+ function test_fuzz_validSignerCount(uint256 _count) public {
+ uint256 count = bound(_count, 0, signerAddresses.length);
+ _addSignersSameHat(count, signerHat);
- function testSetTargetThresholdUpdatesThresholdCorrectly() public {
- // set target threshold to 5
- mockIsWearerCall(address(this), ownerHat, true);
- hatsSignerGate.setTargetThreshold(5);
- // add 5 valid signers
- addSigners(5);
- // one loses their hat
- mockIsWearerCall(addresses[4], signerHat, false);
- // lower target threshold to 4
- hatsSignerGate.setTargetThreshold(4);
- // since hatsSignerGate.validSignerCount() is also 4, the threshold should also be 4
- assertEq(safe.getThreshold(), 4, "threshold");
- }
+ assertEq(instance.validSignerCount(), count, "valid signer count should be correct");
+ }
- function testSetTargetTresholdCannotSetBelowMinThreshold() public {
- assertEq(hatsSignerGate.minThreshold(), 2, "min threshold");
- assertEq(hatsSignerGate.targetThreshold(), 2, "target threshold");
+ function test_fuzz_canAttachToSafe() public {
+ // deploy an instance with
+ // deploy a new safe
+ ISafe newSafe = _deploySafe(signerAddresses, 1, 1);
- // set target threshold to 1 — should fail
- mockIsWearerCall(address(this), ownerHat, true);
- vm.expectRevert(InvalidTargetThreshold.selector);
- hatsSignerGate.setTargetThreshold(1);
- }
+ // the new safe should be attachable since it has no modules
+ assertTrue(instance.canAttachToSafe(newSafe), "should be attachable");
+ }
- function testCannotAccidentallySetThresholdHigherThanTarget() public {
- assertEq(hatsSignerGate.targetThreshold(), 2, "target threshold");
-
- // to reach the condition to test, we need...
- // 1) signer count > target threshold
- // 2) current threshold < target threshold
-
- // 1) its unlikely to get both of these naturally since adding new signers increases the threshold
- // but we can force it by adding owners to the safe by pretending to be the hatsSignerGate itself
- // we start by adding 1 valid signer legitimately
- addSigners(1);
- // then we add 2 more valid owners my pranking the execTransactionFromModule function
- mockIsWearerCall(addresses[2], signerHat, true);
- mockIsWearerCall(addresses[3], signerHat, true);
- bytes memory addOwner3 = abi.encodeWithSignature("addOwnerWithThreshold(address,uint256)", addresses[2], 1);
- bytes memory addOwner4 = abi.encodeWithSignature("addOwnerWithThreshold(address,uint256)", addresses[3], 1);
-
- // mockIsWearerCall(address(this), signerHat, true);
- vm.startPrank(address(hatsSignerGate));
- safe.execTransactionFromModule(
- address(safe), // to
- 0, // value
- addOwner3, // data
- Enum.Operation.Call // operation
- );
- safe.execTransactionFromModule(
- address(safe), // to
- 0, // value
- addOwner4, // data
- Enum.Operation.Call // operation
- );
-
- // now we've meet the necessary conditions
- assertGt(
- hatsSignerGate.validSignerCount(), hatsSignerGate.targetThreshold(), "1) signer count > target threshold"
- );
- assertLt(safe.getThreshold(), hatsSignerGate.targetThreshold(), "2) current threshold < target threshold");
-
- // calling reconcile should change the threshold to the target
- hatsSignerGate.reconcileSignerCount();
- assertEq(safe.getThreshold(), hatsSignerGate.targetThreshold(), "threshold == target threshold");
- }
+ function test_false_canAttachToSafe(uint256 _seed) public {
+ // deploy an instance with
+ // deploy a new safe
+ ISafe newSafe = _deploySafe(signerAddresses, 1, 1);
- function testAttackerCannotExploitSigHandlingDifferences() public {
- // start with 4 valid signers
- addSigners(4);
- // set target threshold (and therefore actual threshold) to 3
- mockIsWearerCall(address(this), ownerHat, true);
- hatsSignerGate.setTargetThreshold(3);
- assertEq(safe.getThreshold(), 3, "initial threshold");
- assertEq(safe.nonce(), 0, "pre nonce");
- // invalidate the 3rd signer, who will be our attacker
- address attacker = addresses[2];
- mockIsWearerCall(attacker, signerHat, false);
-
- // Attacker crafts a tx to submit to the safe.
- address maliciousContract = makeAddr("maliciousContract");
- bytes memory maliciousTx = abi.encodeWithSignature("maliciousCall(uint256)", 1 ether);
- // Attacker gets 2 of the valid signers to sign it, and adds their own (invalid) signature: NSigs = 3
- bytes32 txHash = safe.getTransactionHash(
- address(safe), // to
- 0, // value
- maliciousTx, // data
- Enum.Operation.Call, // operation
- 0, // safeTxGas
- 0, // baseGas
- 0, // gasPrice
- address(0), // gasToken
- address(0), // refundReceiver
- safe.nonce() // nonce
- );
- bytes memory sigs = createNSigsForTx(txHash, 3);
-
- // attacker adds a contract signature from the 4th signer from a previous tx
- // since HSG doesn't check that the correct data was signed, it would be considered a valid signature
- bytes memory contractSig = abi.encode(addresses[3], bytes32(0), bytes1(0x01));
- sigs = bytes.concat(sigs, contractSig);
-
- // mock the maliciousTx so it would succeed if it were to be executed
- vm.mockCall(maliciousContract, maliciousTx, abi.encode(true));
- // attacker submits the tx to the safe, but it should fail
- vm.expectRevert(InvalidSigners.selector);
- vm.prank(attacker);
- safe.execTransaction(
- address(safe),
- 0,
- maliciousTx,
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- // (r,s,v) [r - from] [s - unused] [v - 1 flag for onchain approval]
- sigs
- );
-
- assertEq(safe.getThreshold(), 3, "post threshold");
- assertEq(hatsSignerGate.validSignerCount(), 3, "valid signer count");
- assertEq(safe.nonce(), 0, "post nonce hasn't changed");
- }
+ // enable a random module on the new safe
+ address module = _getRandomAddress(_seed);
+ vm.prank(address(newSafe));
+ newSafe.enableModule(module);
- function testSignersCannotReenterCheckTransactionToAddOwners() public {
- address newOwner = makeAddr("newOwner");
- bytes memory addOwnerAction;
- bytes memory sigs;
- bytes memory checkTxAction;
- bytes memory multisend;
- // start with 3 valid signers
- addSigners(3);
- // attacker is the first of these signers
- address attacker = addresses[0];
- assertEq(safe.getThreshold(), 2, "initial threshold");
- assertEq(safe.getOwners().length, 3, "initial owner count");
-
- /* attacker crafts a multisend tx to submit to the safe, with the following actions:
- 1) add a new owner
- — when `HSG.checkTransaction` is called, the hash of the original owner array will be stored
- 2) directly call `HSG.checkTransaction`
- — this will cause the hash of the new owner array (with the new owner from #1) to be stored
- — when `HSG.checkAfterExecution` is called, the owner array check will pass even though
- */
-
- // 1) craft the addOwner action
- // mock the new owner as a valid signer
- mockIsWearerCall(newOwner, signerHat, true);
- {
- // use scope to avoid stack too deep error
- // compile the action
- addOwnerAction = abi.encodeWithSignature("addOwnerWithThreshold(address,uint256)", newOwner, 2);
-
- // 2) craft the direct checkTransaction action
- // first craft a dummy/empty tx to pass to checkTransaction
- bytes32 dummyTxHash = safe.getTransactionHash(
- attacker, // send 0 eth to the attacker
- 0,
- hex"00",
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- address(0),
- safe.nonce()
- );
-
- // then have it signed by the attacker and a collaborator
- sigs = createNSigsForTx(dummyTxHash, 2);
-
- checkTxAction = abi.encodeWithSelector(
- hatsSignerGate.checkTransaction.selector,
- // checkTransaction params
- attacker,
- 0,
- hex"00",
- Enum.Operation.Call,
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- sigs,
- attacker // msgSender
- );
-
- // now bundle the two actions into a multisend tx
- bytes memory packedCalls = abi.encodePacked(
- // 1) add owner
- uint8(0), // 0 for call; 1 for delegatecall
- safe, // to
- uint256(0), // value
- uint256(addOwnerAction.length), // data length
- bytes(addOwnerAction), // data
- // 2) direct call to checkTransaction
- uint8(0), // 0 for call; 1 for delegatecall
- hatsSignerGate, // to
- uint256(0), // value
- uint256(checkTxAction.length), // data length
- bytes(checkTxAction) // data
- );
- multisend = abi.encodeWithSignature("multiSend(bytes)", packedCalls);
- }
-
- // now get the safe tx hash and have attacker sign it with a collaborator
- bytes32 safeTxHash = safe.getTransactionHash(
- gnosisMultisendLibrary, // to
- 0, // value
- multisend, // data
- Enum.Operation.DelegateCall, // operation
- 0, // safeTxGas
- 0, // baseGas
- 0, // gasPrice
- address(0), // gasToken
- address(0), // refundReceiver
- safe.nonce() // nonce
- );
- sigs = createNSigsForTx(safeTxHash, 2);
-
- // now submit the tx to the safe
- vm.prank(attacker);
- /*
- Expect revert because of re-entry into checkTransaction
- While hatsSignerGate will throw the NoReentryAllowed error,
- since the error occurs within the context of the safe transaction,
- the safe will catch the error and re-throw with its own error,
- ie `GS013` ("Safe transaction failed when gasPrice and safeTxGas were 0")
- */
- vm.expectRevert(bytes("GS013"));
- safe.execTransaction(
- gnosisMultisendLibrary,
- 0,
- multisend,
- Enum.Operation.DelegateCall,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- sigs
- );
-
- // no new owners have been added, despite the attacker's best efforts
- assertEq(safe.getOwners().length, 3, "post owner count");
- }
+ // the new safe should not be attachable since it has a module
+ assertFalse(instance.canAttachToSafe(newSafe), "should not be attachable");
+ }
}
diff --git a/test/HatsSignerGateFactory.t.sol b/test/HatsSignerGateFactory.t.sol
deleted file mode 100644
index bd8f1ea..0000000
--- a/test/HatsSignerGateFactory.t.sol
+++ /dev/null
@@ -1,381 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-import "./HSGFactoryTestSetup.t.sol";
-
-contract HatsSignerGateFactoryTest is HSGFactoryTestSetup {
- error NoOtherModulesAllowed();
-
- function setUp() public {
- version = "1.0";
-
- factory = new HatsSignerGateFactory(
- address(singletonHatsSignerGate),
- address(singletonMultiHatsSignerGate),
- HATS,
- address(singletonSafe),
- gnosisFallbackLibrary,
- gnosisMultisendLibrary,
- address(safeFactory),
- address(moduleProxyFactory),
- version
- );
- }
-
- function testDeployFactory() public {
- assertEq(factory.version(), version);
- assertEq(factory.hatsSignerGateSingleton(), address(singletonHatsSignerGate));
- assertEq(factory.multiHatsSignerGateSingleton(), address(singletonMultiHatsSignerGate));
- assertEq(address(factory.safeSingleton()), address(singletonSafe));
- assertEq(factory.gnosisFallbackLibrary(), gnosisFallbackLibrary);
- assertEq(factory.gnosisMultisendLibrary(), gnosisMultisendLibrary);
- assertEq(address(factory.gnosisSafeProxyFactory()), address(safeFactory));
- }
-
- function testDeployHatsSignerGate() public {
- ownerHat = uint256(1);
- signerHat = uint256(2);
- minThreshold = 2;
- targetThreshold = 2;
- maxSigners = 5;
-
- // deploy a safe
- initSafeOwners[0] = address(this);
- safe = deploySafe(initSafeOwners, 1);
-
- hatsSignerGate = HatsSignerGate(
- factory.deployHatsSignerGate(ownerHat, signerHat, address(safe), minThreshold, targetThreshold, maxSigners)
- );
-
- assertEq(safe.getOwners()[0], address(this));
-
- assertEq(hatsSignerGate.minThreshold(), minThreshold);
- assertEq(hatsSignerGate.ownerHat(), ownerHat);
- assertEq(hatsSignerGate.getHatsContract(), HATS);
- assertEq(hatsSignerGate.targetThreshold(), targetThreshold);
- assertEq(address(hatsSignerGate.safe()), address(safe));
- assertEq(hatsSignerGate.maxSigners(), maxSigners);
- assertEq(hatsSignerGate.version(), version);
- }
-
- function testDeployHatsSignersGateAndSafe(
- uint256 _ownerHat,
- uint256 _signerHat,
- uint256 _minThreshold,
- uint256 _targetThreshold,
- uint256 _maxSigners
- ) public {
- vm.assume(_ownerHat > 0);
- ownerHat = _ownerHat;
-
- vm.assume(_signerHat > 0);
- signerHat = _signerHat;
-
- vm.assume(_maxSigners > 1);
- maxSigners = _maxSigners;
-
- vm.assume(_targetThreshold <= maxSigners);
- targetThreshold = _targetThreshold;
-
- vm.assume(_minThreshold <= targetThreshold);
- minThreshold = _minThreshold;
-
- (hatsSignerGate, safe) = deployHSGAndSafe(ownerHat, signerHat, minThreshold, targetThreshold, maxSigners);
-
- assertEq(safe.getOwners()[0], address(hatsSignerGate));
-
- assertEq(hatsSignerGate.minThreshold(), minThreshold);
- assertEq(hatsSignerGate.targetThreshold(), targetThreshold);
- assertEq(address(hatsSignerGate.safe()), address(safe));
- assertEq(hatsSignerGate.maxSigners(), maxSigners);
- assertEq(hatsSignerGate.version(), version);
-
- assertTrue(safe.isModuleEnabled(address(hatsSignerGate)));
-
- assertEq(address(bytes20(vm.load(address(safe), GUARD_STORAGE_SLOT) << 96)), address(hatsSignerGate));
-
- assertEq(hatsSignerGate.ownerHat(), ownerHat);
- assertEq(hatsSignerGate.getHatsContract(), HATS);
- }
-
- function testCannotReinitializeHSGSingleton() public {
- bytes memory initializeParams =
- abi.encode(ownerHat, signerHat, address(safe), HATS, minThreshold, targetThreshold, maxSigners, version, 0);
- vm.expectRevert("Initializable: contract is already initialized");
- singletonHatsSignerGate.setUp(initializeParams);
- }
-
- function testDeployMultiHatsSignerGate() public {
- ownerHat = 1;
- uint256[] memory signerHats = new uint256[](1);
- signerHats[0] = 2;
- minThreshold = 2;
- targetThreshold = 2;
- maxSigners = 5;
-
- // deploy a safe
- initSafeOwners[0] = address(this);
- safe = deploySafe(initSafeOwners, 1);
-
- multiHatsSignerGate = MultiHatsSignerGate(
- factory.deployMultiHatsSignerGate(
- ownerHat, signerHats, address(safe), minThreshold, targetThreshold, maxSigners
- )
- );
-
- assertEq(safe.getOwners()[0], address(this));
-
- assertEq(multiHatsSignerGate.minThreshold(), minThreshold);
- assertEq(multiHatsSignerGate.ownerHat(), ownerHat);
- assertEq(multiHatsSignerGate.getHatsContract(), HATS);
- assertEq(multiHatsSignerGate.targetThreshold(), targetThreshold);
- assertEq(address(multiHatsSignerGate.safe()), address(safe));
- assertEq(multiHatsSignerGate.maxSigners(), maxSigners);
- assertEq(multiHatsSignerGate.version(), version);
- assertTrue(multiHatsSignerGate.isValidSignerHat(2));
- assertFalse(multiHatsSignerGate.isValidSignerHat(3));
- }
-
- function testDeployHatsSignersGateAndSafe() public {
- ownerHat = 1;
- uint256[] memory signerHats = new uint256[](1);
- signerHats[0] = 2;
- minThreshold = 2;
- targetThreshold = 2;
- maxSigners = 5;
-
- (multiHatsSignerGate, safe) = deployMHSGAndSafe(ownerHat, signerHats, minThreshold, targetThreshold, maxSigners);
-
- assertEq(safe.getOwners()[0], address(multiHatsSignerGate));
-
- assertEq(multiHatsSignerGate.minThreshold(), minThreshold);
- assertEq(multiHatsSignerGate.targetThreshold(), targetThreshold);
- assertEq(address(multiHatsSignerGate.safe()), address(safe));
- assertEq(multiHatsSignerGate.maxSigners(), maxSigners);
- assertEq(multiHatsSignerGate.version(), version);
- assertTrue(multiHatsSignerGate.isValidSignerHat(2));
- assertFalse(multiHatsSignerGate.isValidSignerHat(3));
-
- assertTrue(safe.isModuleEnabled(address(multiHatsSignerGate)));
-
- assertEq(address(bytes20(vm.load(address(safe), GUARD_STORAGE_SLOT) << 96)), address(multiHatsSignerGate));
-
- assertEq(multiHatsSignerGate.ownerHat(), ownerHat);
- assertEq(multiHatsSignerGate.getHatsContract(), HATS);
- }
-
- function testCannotReinitializeMHSGSingleton() public {
- uint256[] memory signerHats = new uint256[](1);
- signerHats[0] = signerHat;
-
- bytes memory initializeParams =
- abi.encode(ownerHat, signerHats, address(safe), HATS, minThreshold, targetThreshold, maxSigners, version, 0);
- vm.expectRevert("Initializable: contract is already initialized");
- singletonMultiHatsSignerGate.setUp(initializeParams);
- }
-
- function testCannotDeployHSGToSafeWithExistingModules() public {
- ownerHat = 1;
- signerHat = 2;
- minThreshold = 2;
- targetThreshold = 2;
- maxSigners = 5;
-
- // deploy a safe to a signer
- initSafeOwners[0] = address(this);
- safe = deploySafe(initSafeOwners, 1);
-
- // add a module
- bytes memory addModuleData = abi.encodeWithSignature("enableModule(address)", address(0xf00baa)); // some devs are from Boston
- executeSafeTxFrom(address(this), addModuleData, safe);
-
- // attempt to deploy HSG, should revert
- vm.expectRevert(NoOtherModulesAllowed.selector);
- factory.deployHatsSignerGate(ownerHat, signerHat, address(safe), minThreshold, targetThreshold, maxSigners);
- }
-
- function testCannotDeployMHSGToSafeWithExistingModules() public {
- ownerHat = 1;
- uint256[] memory signerHats = new uint256[](1);
- signerHats[0] = 2;
- minThreshold = 2;
- targetThreshold = 2;
- maxSigners = 5;
-
- // deploy a safe to a signer
- initSafeOwners[0] = address(this);
- safe = deploySafe(initSafeOwners, 1);
-
- // add a module
- bytes memory addModuleData = abi.encodeWithSignature("enableModule(address)", address(0xf00baa)); // some devs are from Boston
- executeSafeTxFrom(address(this), addModuleData, safe);
-
- // attempt to deploy HSG, should revert
- vm.expectRevert(NoOtherModulesAllowed.selector);
- factory.deployMultiHatsSignerGate(
- ownerHat, signerHats, address(safe), minThreshold, targetThreshold, maxSigners
- );
- }
-
- function testCanAttachHSGToSafeReturnsFalseWithModule() public {
- ownerHat = 1;
- signerHat = 2;
- minThreshold = 2;
- targetThreshold = 2;
- maxSigners = 5;
-
- // deploy a safe
- initSafeOwners[0] = address(this);
- safe = deploySafe(initSafeOwners, 1);
-
- // deploy HSG
- address hsg =
- factory.deployHatsSignerGate(ownerHat, signerHat, address(safe), minThreshold, targetThreshold, maxSigners);
-
- // enable a module
- bytes memory enableModuleData = abi.encodeWithSignature("enableModule(address)", address(0xf00baa)); // some devs are from Boston
- executeSafeTxFrom(address(this), enableModuleData, safe);
-
- // canAttachHSGToSafe should return false
- assertFalse(factory.canAttachHSGToSafe(HatsSignerGate(hsg)));
- }
-
- function testCanAttachHSGToSafeReturnsFalseWithUnsafeSignerCounts() public {
- ownerHat = 1;
- signerHat = 2;
- minThreshold = 1;
- targetThreshold = 1;
- maxSigners = 2;
-
- // deploy a safe
- initSafeOwners[0] = address(this);
- mockIsWearerCall(address(this), signerHat, true);
- safe = deploySafe(initSafeOwners, 1);
-
- // deploy HSG
- HatsSignerGate hsg = HatsSignerGate(
- factory.deployHatsSignerGate(ownerHat, signerHat, address(safe), minThreshold, targetThreshold, maxSigners)
- );
-
- // add 2 owners and make them valid
- bytes memory addOwnerData = abi.encodeWithSignature("addOwnerWithThreshold(address,uint256)", address(2), 1);
- executeSafeTxFrom(address(this), addOwnerData, safe);
- mockIsWearerCall(address(2), signerHat, true);
-
- addOwnerData = abi.encodeWithSignature("addOwnerWithThreshold(address,uint256)", address(3), 1);
- executeSafeTxFrom(address(this), addOwnerData, safe);
- mockIsWearerCall(address(3), signerHat, true);
-
- // canAttachHSGToSafe should return false
- assertEq(hsg.validSignerCount(), 3, "valid signer count");
- assertEq(hsg.maxSigners(), 2, "max signers");
- assertFalse(
- factory.canAttachHSGToSafe(HatsSignerGate(hsg)), "should return false with validSignerCount > maxSigners"
- );
- }
-
- function testCanAttachHSGToSafeReturnsTrue() public {
- ownerHat = 1;
- signerHat = 2;
- minThreshold = 2;
- targetThreshold = 2;
- maxSigners = 5;
-
- // deploy a safe
- initSafeOwners[0] = address(this);
- mockIsWearerCall(address(this), signerHat, true);
- safe = deploySafe(initSafeOwners, 1);
-
- // deploy HSG
- address hsg =
- factory.deployHatsSignerGate(ownerHat, signerHat, address(safe), minThreshold, targetThreshold, maxSigners);
-
- // canAttachHSGToSafe should return true
- assertTrue(factory.canAttachHSGToSafe(HatsSignerGate(hsg)));
- }
-
- function testCanAttachMHSGToSafeReturnsFalseWithModule() public {
- ownerHat = 1;
- minThreshold = 2;
- targetThreshold = 2;
- maxSigners = 5;
- // create signerHats array
- uint256[] memory signerHats = new uint256[](1);
- signerHats[0] = 2;
-
- // deploy a safe
- initSafeOwners[0] = address(this);
- safe = deploySafe(initSafeOwners, 1);
-
- // deploy MHSG
- address mhsg = factory.deployMultiHatsSignerGate(
- ownerHat, signerHats, address(safe), minThreshold, targetThreshold, maxSigners
- );
-
- // enable a module
- bytes memory enableModuleData = abi.encodeWithSignature("enableModule(address)", address(0xf00baa)); // some devs are from Boston
- executeSafeTxFrom(address(this), enableModuleData, safe);
-
- // canAttachHSGToSafe should return false
- assertFalse(factory.canAttachMHSGToSafe(MultiHatsSignerGate(mhsg)));
- }
-
- function testCanAttachMHSGToSafeReturnsFalseWithUnsafeSignerCounts() public {
- ownerHat = 1;
- minThreshold = 1;
- targetThreshold = 1;
- maxSigners = 2;
- // create signerHats array
- uint256[] memory signerHats = new uint256[](1);
- signerHats[0] = 2;
-
- // deploy a safe
- initSafeOwners[0] = address(this);
- mockIsWearerCall(address(this), signerHat, true);
- safe = deploySafe(initSafeOwners, 1);
-
- // deploy MHSG
- MultiHatsSignerGate mhsg = MultiHatsSignerGate(
- factory.deployMultiHatsSignerGate(
- ownerHat, signerHats, address(safe), minThreshold, targetThreshold, maxSigners
- )
- );
-
- // add 2 owners and make them valid
- bytes memory addOwnerData = abi.encodeWithSignature("addOwnerWithThreshold(address,uint256)", address(2), 1);
- executeSafeTxFrom(address(this), addOwnerData, safe);
- mockIsWearerCall(address(2), signerHat, true);
-
- addOwnerData = abi.encodeWithSignature("addOwnerWithThreshold(address,uint256)", address(3), 1);
- executeSafeTxFrom(address(this), addOwnerData, safe);
- mockIsWearerCall(address(3), signerHat, true);
-
- // canAttachHSGToSafe should return false
- assertEq(mhsg.validSignerCount(), 3, "valid signer count");
- assertEq(mhsg.maxSigners(), 2, "max signers");
- assertFalse(factory.canAttachMHSGToSafe(mhsg), "should return false with validSignerCount > maxSigners");
- }
-
- function testCanAttachMHSGToSafeReturnsTrue() public {
- ownerHat = 1;
- minThreshold = 2;
- targetThreshold = 2;
- maxSigners = 5;
- // create signerHats array
- uint256[] memory signerHats = new uint256[](1);
- signerHats[0] = 2;
-
- // deploy a safe
- initSafeOwners[0] = address(this);
- mockIsWearerCall(address(this), signerHat, true);
- safe = deploySafe(initSafeOwners, 1);
-
- // deploy MHSG
- address mhsg = factory.deployMultiHatsSignerGate(
- ownerHat, signerHats, address(safe), minThreshold, targetThreshold, maxSigners
- );
-
- // canAttachHSGToSafe should return true
- assertTrue(factory.canAttachMHSGToSafe(MultiHatsSignerGate(mhsg)));
- }
-}
diff --git a/test/MHSGTestSetup.t.sol b/test/MHSGTestSetup.t.sol
deleted file mode 100644
index 7c47b83..0000000
--- a/test/MHSGTestSetup.t.sol
+++ /dev/null
@@ -1,53 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-import "./HSGTestSetup.t.sol";
-import "../src/HSGLib.sol";
-import "@gnosis.pm/safe-contracts/contracts/common/SignatureDecoder.sol";
-import "@gnosis.pm/safe-contracts/contracts/common/Enum.sol";
-
-contract MHSGTestSetup is HSGTestSetup {
- uint256[] public signerHats;
-
- function setUp() public override {
- // set up variables
- ownerHat = 1;
- signerHats = new uint256[](5);
- signerHats[0] = 2;
- signerHats[1] = 3;
- signerHats[2] = 4;
- signerHats[3] = 5;
- signerHats[4] = 6;
- minThreshold = 2;
- targetThreshold = 2;
- maxSigners = 5;
-
- (pks, addresses) = createAddressesFromPks(6);
-
- version = "1.0";
-
- factory = new HatsSignerGateFactory(
- address(singletonHatsSignerGate),
- address(singletonMultiHatsSignerGate),
- HATS,
- address(singletonSafe),
- gnosisFallbackLibrary,
- gnosisMultisendLibrary,
- address(safeFactory),
- address(moduleProxyFactory),
- version
- );
-
- (multiHatsSignerGate, safe) = deployMHSGAndSafe(ownerHat, signerHats, minThreshold, targetThreshold, maxSigners);
- mockIsWearerCall(address(multiHatsSignerGate), signerHat, false);
- }
-
- function addSigners_Multi(uint256 count) internal {
- for (uint256 i = 0; i < count; i++) {
- console2.log("signersHats[i]", signerHats[i]);
- mockIsWearerCall(addresses[i], signerHats[i], true);
- vm.prank(addresses[i]);
- multiHatsSignerGate.claimSigner(signerHats[i]);
- }
- }
-}
diff --git a/test/MultiHatsSignerGate.t.sol b/test/MultiHatsSignerGate.t.sol
deleted file mode 100644
index d92a912..0000000
--- a/test/MultiHatsSignerGate.t.sol
+++ /dev/null
@@ -1,183 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-import "./MHSGTestSetup.t.sol";
-
-contract MultiHatsSignerGateTest is MHSGTestSetup {
- function test_Multi_AddSingleSigner() public {
- addSigners_Multi(1);
-
- assertEq(multiHatsSignerGate.validSignerCount(), 1);
- assertEq(safe.getOwners()[0], addresses[0]);
- assertEq(safe.getThreshold(), 1);
- }
-
- function test_Multi_AddTwoSigners_DifferentHats() public {
- addSigners_Multi(2);
-
- assertEq(multiHatsSignerGate.validSignerCount(), 2);
- assertEq(safe.getOwners()[0], addresses[1]);
- assertEq(safe.getOwners()[1], addresses[0]);
- assertEq(safe.getThreshold(), 2);
- }
-
- function test_Multi_NonHatWearerCannotClaimSigner(uint256 i) public {
- vm.assume(i < 2);
- mockIsWearerCall(addresses[3], signerHats[i], false);
-
- vm.prank(addresses[3]);
-
- vm.expectRevert(abi.encodeWithSelector(NotSignerHatWearer.selector, addresses[3]));
- multiHatsSignerGate.claimSigner(signerHats[i]);
- }
-
- function test_Multi_CanRemoveInvalidSigner1() public {
- addSigners_Multi(1);
-
- mockIsWearerCall(addresses[0], signerHats[0], false);
-
- multiHatsSignerGate.removeSigner(addresses[0]);
-
- assertEq(safe.getOwners().length, 1);
- assertEq(safe.getOwners()[0], address(multiHatsSignerGate));
- assertEq(multiHatsSignerGate.validSignerCount(), 0);
- assertEq(safe.getThreshold(), 1);
- }
-
- function test_Multi_CannotRemoveValidSigner() public {
- addSigners_Multi(1);
-
- mockIsWearerCall(addresses[0], signerHats[0], true);
-
- vm.expectRevert(abi.encodeWithSelector(StillWearsSignerHat.selector, addresses[0]));
-
- multiHatsSignerGate.removeSigner(addresses[0]);
-
- assertEq(safe.getOwners().length, 1);
- assertEq(safe.getOwners()[0], addresses[0]);
- assertEq(multiHatsSignerGate.validSignerCount(), 1);
-
- assertEq(safe.getThreshold(), 1);
- }
-
- function test_Multi_ExecTxByHatWearers() public {
- addSigners_Multi(3);
-
- uint256 preNonce = safe.nonce();
- uint256 preValue = 1 ether;
- uint256 transferValue = 0.2 ether;
- uint256 postValue = preValue - transferValue;
- address destAddress = addresses[3];
- // give the safe some eth
- hoax(address(safe), preValue);
-
- // create the tx
- bytes32 txHash = getTxHash(destAddress, transferValue, hex"00", safe);
-
- // have 3 signers sign it
- bytes memory signatures = createNSigsForTx(txHash, 3);
-
- // have one of the signers submit/exec the tx
- vm.prank(addresses[0]);
- safe.execTransaction(
- destAddress,
- transferValue,
- hex"00",
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
- // confirm it we executed by checking ETH balance changes
- assertEq(address(safe).balance, postValue);
- assertEq(destAddress.balance, transferValue);
- assertEq(safe.nonce(), preNonce + 1);
- }
-
- function test_Multi_ExecTxByNonHatWearersReverts() public {
- addSigners_Multi(3);
-
- uint256 preNonce = safe.nonce();
- uint256 preValue = 1 ether;
- uint256 transferValue = 0.2 ether;
- // uint256 postValue = preValue - transferValue;
- address destAddress = addresses[3];
- // give the safe some eth
- hoax(address(safe), preValue);
- // emit log_uint(address(safe).balance);
- // create tx to send some eth from safe to wherever
- // create the tx
- bytes32 txHash = getTxHash(destAddress, transferValue, hex"00", safe);
-
- // have 3 signers sign it
- bytes memory signatures = createNSigsForTx(txHash, 3);
-
- // removing the hats from 2 signers
- mockIsWearerCall(addresses[0], signerHats[0], false);
- mockIsWearerCall(addresses[1], signerHats[1], false);
-
- // emit log_uint(address(safe).balance);
- // have one of the signers submit/exec the tx
- vm.prank(addresses[0]);
-
- // vm.expectRevert(abi.encodeWithSelector(BelowMinThreshold.selector, minThreshold, 1));
- vm.expectRevert(InvalidSigners.selector);
-
- safe.execTransaction(
- destAddress,
- transferValue,
- hex"00",
- Enum.Operation.Call,
- // not using the refunder
- 0,
- 0,
- 0,
- address(0),
- payable(address(0)),
- signatures
- );
-
- // confirm it was not executed by checking ETH balance changes
- assertEq(destAddress.balance, 0);
- assertEq(safe.nonce(), preNonce);
- }
-
- function test_Multi_OwnerCanAddSignerHats(uint256 count) public {
- vm.assume(count < 100);
-
- // create and fill an array of signer hats to add, with length = count
- uint256[] memory hats = new uint256[](count);
- for (uint256 i; i < count; ++i) {
- hats[i] = i;
- }
-
- mockIsWearerCall(addresses[0], multiHatsSignerGate.ownerHat(), true);
- vm.prank(addresses[0]);
-
- vm.expectEmit(false, false, false, true);
- emit HSGLib.SignerHatsAdded(hats);
-
- multiHatsSignerGate.addSignerHats(hats);
- }
-
- function test_Multi_OwnerCanAddSignerHats1() public {
- test_Multi_OwnerCanAddSignerHats(1);
- }
-
- function test_Multi_NonOwnerCannotAddSignerHats() public {
- // create and fill an array of signer hats to add, with length = 1
- uint256[] memory hats = new uint256[](1);
- hats[0] = 1;
-
- mockIsWearerCall(addresses[0], multiHatsSignerGate.ownerHat(), false);
- vm.prank(addresses[0]);
-
- vm.expectRevert("UNAUTHORIZED");
-
- multiHatsSignerGate.addSignerHats(hats);
- }
-}
diff --git a/test/SafeManagerLib.t.sol b/test/SafeManagerLib.t.sol
new file mode 100644
index 0000000..c679740
--- /dev/null
+++ b/test/SafeManagerLib.t.sol
@@ -0,0 +1,289 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.13;
+
+import { Test, console2 } from "forge-std/Test.sol";
+import { TestSuite } from "./TestSuite.t.sol";
+import { SafeManagerLib } from "../src/lib/SafeManagerLib.sol";
+import { ISafe, IModuleManager, IGuardManager, IOwnerManager } from "../src/lib/safe-interfaces/ISafe.sol";
+import { WithHSGInstanceTest, WithHSGHarnessInstanceTest } from "./TestSuite.t.sol";
+import { HatsSignerGateHarness } from "./harnesses/HatsSignerGateHarness.sol";
+import { ModuleProxyFactory } from "../lib/zodiac/contracts/factory/ModuleProxyFactory.sol";
+
+contract SafeManagerLib_EncodingActions is Test {
+ function test_fuzz_encodeEnableModuleAction(address _moduleToEnable) public pure {
+ // Generate the encoded data using SafeManagerLib
+ bytes memory encodedData = SafeManagerLib.encodeEnableModuleAction(_moduleToEnable);
+
+ // Generate the expected encoded data manually
+ bytes memory expectedData = abi.encodeWithSelector(IModuleManager.enableModule.selector, _moduleToEnable);
+
+ // Assert the encoded data matches the expected data
+ assertEq(encodedData, expectedData);
+ }
+
+ function test_fuzz_encodeDisableModuleAction(address _previousModule, address _moduleToDisable) public pure {
+ // Generate the encoded data using SafeManagerLib
+ bytes memory encodedData = SafeManagerLib.encodeDisableModuleAction(_previousModule, _moduleToDisable);
+
+ // Generate the expected encoded data manually
+ bytes memory expectedData =
+ abi.encodeWithSelector(IModuleManager.disableModule.selector, _previousModule, _moduleToDisable);
+
+ // Assert the encoded data matches the expected data
+ assertEq(encodedData, expectedData);
+ }
+
+ function test_fuzz_encodeSetGuardAction(address _guardToSet) public pure {
+ // Generate the encoded data using SafeManagerLib
+ bytes memory encodedData = SafeManagerLib.encodeSetGuardAction(_guardToSet);
+
+ // Generate the expected encoded data manually
+ bytes memory expectedData = abi.encodeWithSelector(IGuardManager.setGuard.selector, _guardToSet);
+
+ // Assert the encoded data matches the expected data
+ assertEq(encodedData, expectedData);
+ }
+
+ function test_fuzz_encodeRemoveHSGAsGuardAction() public pure {
+ // Generate the encoded data using SafeManagerLib
+ bytes memory encodedData = SafeManagerLib.encodeRemoveHSGAsGuardAction();
+
+ // Generate the expected encoded data manually - setting guard to the zero address to remove it
+ bytes memory expectedData = abi.encodeWithSelector(IGuardManager.setGuard.selector, address(0));
+
+ // Assert the encoded data matches the expected data
+ assertEq(encodedData, expectedData);
+ }
+
+ function test_fuzz_encodeSwapOwnerAction(address _prevOwner, address _oldOwner, address _newOwner) public pure {
+ // Generate the encoded data using SafeManagerLib
+ bytes memory encodedData = SafeManagerLib.encodeSwapOwnerAction(_prevOwner, _oldOwner, _newOwner);
+
+ // Generate the expected encoded data manually
+ bytes memory expectedData =
+ abi.encodeWithSelector(IOwnerManager.swapOwner.selector, _prevOwner, _oldOwner, _newOwner);
+
+ // Assert the encoded data matches the expected data
+ assertEq(encodedData, expectedData);
+ }
+
+ function test_fuzz_encodeRemoveOwnerAction(address _prevOwner, address _oldOwner, uint256 _newThreshold) public pure {
+ // Generate the encoded data using SafeManagerLib
+ bytes memory encodedData = SafeManagerLib.encodeRemoveOwnerAction(_prevOwner, _oldOwner, _newThreshold);
+
+ // Generate the expected encoded data manually
+ bytes memory expectedData =
+ abi.encodeWithSelector(IOwnerManager.removeOwner.selector, _prevOwner, _oldOwner, _newThreshold);
+
+ // Assert the encoded data matches the expected data
+ assertEq(encodedData, expectedData);
+ }
+
+ function test_fuzz_encodeAddOwnerWithThresholdAction(address _owner, uint256 _newThreshold) public pure {
+ // Generate the encoded data using SafeManagerLib
+ bytes memory encodedData = SafeManagerLib.encodeAddOwnerWithThresholdAction(_owner, _newThreshold);
+
+ // Generate the expected encoded data manually
+ bytes memory expectedData =
+ abi.encodeWithSelector(IOwnerManager.addOwnerWithThreshold.selector, _owner, _newThreshold);
+
+ // Assert the encoded data matches the expected data
+ assertEq(encodedData, expectedData);
+ }
+
+ function test_fuzz_encodeChangeThresholdAction(uint256 _newThreshold) public pure {
+ // Generate the encoded data using SafeManagerLib
+ bytes memory encodedData = SafeManagerLib.encodeChangeThresholdAction(_newThreshold);
+
+ // Generate the expected encoded data manually
+ bytes memory expectedData = abi.encodeWithSelector(IOwnerManager.changeThreshold.selector, _newThreshold);
+
+ // Assert the encoded data matches the expected data
+ assertEq(encodedData, expectedData);
+ }
+}
+
+contract SafeManagerLib_ExecutingActions is WithHSGHarnessInstanceTest {
+ /// @dev Since execSafeTransactionFromHSG is called by all the other exec* functions, we rely on tests for those
+ /// functions to verify that execSafeTransactionFromHSG is working correctly.
+ // TODO: add test for execSafeTransactionFromHSG
+
+ function test_execDisableHSGAsOnlyModule() public {
+ harness.execDisableHSGAsOnlyModule(safe);
+
+ assertFalse(safe.isModuleEnabled(address(instance)), "hsg should no longer be a module");
+ }
+
+ function test_fuzz_execDisableHSGAsModule(uint256 _otherModulesCount) public {
+ // bound the fuzzing parameters
+ _otherModulesCount = bound(_otherModulesCount, 1, fuzzingAddresses.length - 1);
+
+ address previousModule = fuzzingAddresses[0];
+
+ // enable all the other modules
+ vm.startPrank(address(safe));
+ for (uint256 i = 0; i < _otherModulesCount; i++) {
+ // enable the other modules
+ safe.enableModule(fuzzingAddresses[i]);
+ }
+ vm.stopPrank();
+
+ // disable the HatsSignerGate as a module; the previous module should always be the same reguardless of
+ // how many other modules are enabled
+ harness.execDisableHSGAsModule(safe, previousModule);
+
+ assertFalse(safe.isModuleEnabled(address(harness)), "harness should no longer be a module");
+ }
+
+ function test_execRemoveHSGAsGuard() public {
+ harness.execRemoveHSGAsGuard(safe);
+
+ assertEq(SafeManagerLib.getSafeGuard(safe), address(0), "harness should no longer be a guard");
+ }
+
+ function test_execAttachNewHSG() public {
+ // use a TestGuard as the new HSG, since it implements the necessary IERC165 interface
+ address newHSG = address(tstGuard);
+
+ harness.execAttachNewHSG(safe, newHSG);
+
+ assertEq(SafeManagerLib.getSafeGuard(safe), newHSG, "newHSG should be the guard on the safe");
+ assertTrue(safe.isModuleEnabled(newHSG), "newHSG should be a module on the safe");
+ assertTrue(safe.isModuleEnabled(address(harness)), "harness should still be a module on the safe");
+ }
+
+ function test_fuzz_execChangeThreshold(uint256 _newThreshold) public {
+ // bound the fuzzing parameter
+ _newThreshold = bound(_newThreshold, 1, fuzzingAddresses.length);
+
+ // add enough owners to the dummy safe to change the threshold
+ // the threshold cannot be greater than the number of owners
+ uint256 ownerCount = _newThreshold;
+ vm.startPrank(address(safe));
+ for (uint256 i; i < ownerCount; i++) {
+ // add the owner with a constant threshold of 1
+ safe.addOwnerWithThreshold(fuzzingAddresses[i], 1);
+ }
+ vm.stopPrank();
+
+ // change the threshold
+ harness.execChangeThreshold(safe, _newThreshold);
+
+ assertEq(safe.getThreshold(), _newThreshold, "threshold should be updated");
+ }
+
+ function test_fuzz_fail_execChangeThreshold_tooHigh(uint256 _newThreshold) public {
+ // bound the fuzzing parameter
+ // the new threshold must be high enough to ensure that we can always make it greater than the owner count
+ _newThreshold = bound(_newThreshold, 2, fuzzingAddresses.length);
+
+ // add too few owners to the dummy safe to change the threshold
+ // the threshold cannot be greater than the number of owners
+ uint256 ownerCount = _newThreshold - 1 - 1; // we subtract an additional 1 to account for the existing initial owner
+ vm.startPrank(address(safe));
+ for (uint256 i; i < ownerCount; i++) {
+ safe.addOwnerWithThreshold(fuzzingAddresses[i], 1);
+ }
+ vm.stopPrank();
+
+ // attempt to change the threshold to a value greater than the number of owners
+ // the transaction should revert
+ vm.expectRevert(SafeManagerLib.SafeTransactionFailed.selector);
+ harness.execChangeThreshold(safe, _newThreshold);
+
+ assertEq(safe.getThreshold(), 1, "threshold should not be updated");
+ }
+}
+
+contract SafeManagerLib_DeployingSafeAndAttachingHSG is TestSuite {
+ function test_deploySafeAndAttachHSG() public {
+ HatsSignerGateHarness harness = new HatsSignerGateHarness(
+ address(hats), address(singletonSafe), safeFallbackLibrary, safeMultisendLibrary, address(safeFactory)
+ );
+
+ safe = ISafe(
+ harness.deploySafeAndAttachHSG(
+ address(safeFactory), address(singletonSafe), safeFallbackLibrary, safeMultisendLibrary
+ )
+ );
+
+ assertTrue(safe.isModuleEnabled(address(harness)), "harness should be a module on the safe");
+ assertEq(SafeManagerLib.getSafeGuard(safe), address(harness), "harness should be set as guard");
+ }
+}
+
+contract SafeManagerLib_Views is WithHSGInstanceTest {
+ function test_getSafeGuard() public view {
+ assertEq(SafeManagerLib.getSafeGuard(safe), address(instance), "harness should be set as guard");
+ }
+
+ function test_getSafeFallbackHandler() public {
+ assertEq(
+ SafeManagerLib.getSafeFallbackHandler(safe),
+ safeFallbackLibrary,
+ "fallback handler should be the safeFallbackLibrary"
+ );
+
+ // iterate through fuzzingAddresses and test each
+ vm.startPrank(address(safe));
+ for (uint256 i; i < fuzzingAddresses.length; i++) {
+ // set the fallback handler to the current address
+ safe.setFallbackHandler(fuzzingAddresses[i]);
+
+ assertEq(
+ SafeManagerLib.getSafeFallbackHandler(safe),
+ fuzzingAddresses[i],
+ "fallback handler should be the current iteration's address"
+ );
+ }
+ vm.stopPrank();
+ }
+
+ function test_getModulesWith1() public view {
+ (address[] memory modulesWith1, address next) = SafeManagerLib.getModulesWith1(safe);
+
+ assertEq(modulesWith1.length, 1, "there should be only one module");
+ assertEq(modulesWith1[0], address(instance), "the only module should be the HatsSignerGate");
+ assertEq(next, SafeManagerLib.SENTINELS, "the next pointer should be the sentinel");
+ }
+
+ function test_canAttachHSG_false() public view {
+ assertFalse(
+ SafeManagerLib.canAttachHSG(safe), "instance should not be able to attach, since it is already a module"
+ );
+ }
+
+ function test_canAttachHSG_true() public {
+ // deploy a new Safe with no owners and threshold of 1
+ initSafeOwners[0] = address(this);
+ ISafe freshSafe = _deploySafe(initSafeOwners, 1, TEST_SALT_NONCE);
+
+ assertTrue(SafeManagerLib.canAttachHSG(freshSafe), "instance should be able to attach to freshSafe");
+ }
+
+ function test_findPrevOwner() public {
+ address prevOwner = SafeManagerLib.findPrevOwner(safe.getOwners(), address(instance));
+ assertEq(prevOwner, SENTINELS, "HSG's previous owner should be the sentinel");
+
+ // add a bunch of owners to the safe and test each
+ vm.startPrank(address(safe));
+ address latestOwner = address(instance);
+ for (uint256 i; i < fuzzingAddresses.length; i++) {
+ address thisOwner = fuzzingAddresses[i];
+ // add the new owner (no need to change the threshold)
+ safe.addOwnerWithThreshold(thisOwner, 1);
+
+ // check the previous owner for thisOwner
+ prevOwner = SafeManagerLib.findPrevOwner(safe.getOwners(), thisOwner);
+ // thisOwner's previous owner should always be the sentinel
+ assertEq(prevOwner, SENTINELS, "wrong previous owner for thisOwner");
+
+ // check the previous owner for the latestOwner
+ if (i > 0) latestOwner = fuzzingAddresses[i - 1];
+ prevOwner = SafeManagerLib.findPrevOwner(safe.getOwners(), latestOwner);
+ // latestOwner's previous should be thisOwner
+ assertEq(prevOwner, thisOwner, "wrong previous owner for latestOwner");
+ }
+ vm.stopPrank();
+ }
+}
diff --git a/test/TestSuite.t.sol b/test/TestSuite.t.sol
new file mode 100644
index 0000000..a4d0f40
--- /dev/null
+++ b/test/TestSuite.t.sol
@@ -0,0 +1,1075 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.13;
+
+import { Test, console2 } from "../lib/forge-std/src/Test.sol";
+import { IHats } from "../lib/hats-protocol/src/Interfaces/IHats.sol";
+import { HatsSignerGate, IHatsSignerGate } from "../src/HatsSignerGate.sol";
+import { HatsSignerGateHarness } from "./harnesses/HatsSignerGateHarness.sol";
+import { ISafe } from "../src/lib/safe-interfaces/ISafe.sol";
+import { SafeProxyFactory } from "../lib/safe-smart-account/contracts/proxies/SafeProxyFactory.sol";
+import { Enum } from "../lib/safe-smart-account/contracts/common/Enum.sol";
+import { StorageAccessible } from "../lib/safe-smart-account/contracts/common/StorageAccessible.sol";
+import { ModuleProxyFactory } from "../lib/zodiac/contracts/factory/ModuleProxyFactory.sol";
+import { DeployImplementation, DeployInstance } from "../script/HatsSignerGate.s.sol";
+import { TestGuard } from "./mocks/TestGuard.sol";
+import { MultiSend } from "../lib/safe-smart-account/contracts/libraries/MultiSend.sol";
+
+abstract contract SafeTestHelpers is Test {
+ address public constant SENTINELS = address(0x1);
+ mapping(address => bytes) public walletSigs;
+ uint256[] public pks;
+ address[] public signerAddresses;
+
+ /*//////////////////////////////////////////////////////////////
+ SAFE TEST HELPERS
+ //////////////////////////////////////////////////////////////*/
+
+ function _getEthTransferSafeTxHash(address _to, uint256 _value, ISafe _safe) internal view returns (bytes32 txHash) {
+ return _safe.getTransactionHash(
+ _to,
+ _value,
+ hex"00",
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ _safe.nonce()
+ );
+ }
+
+ function _getTxHash(address _to, uint256 _value, Enum.Operation _operation, bytes memory _data, ISafe _safe)
+ internal
+ view
+ returns (bytes32 txHash)
+ {
+ return _safe.getTransactionHash(
+ _to,
+ _value,
+ _data,
+ _operation,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ _safe.nonce()
+ );
+ }
+
+ function _createNSigsForTx(bytes32 _txHash, uint256 _signerCount) internal returns (bytes memory signatures) {
+ uint8 v;
+ bytes32 r;
+ bytes32 s;
+ address signer;
+ uint256[] memory signers = new uint256[](_signerCount);
+
+ for (uint256 i = 0; i < _signerCount; ++i) {
+ // sign txHash
+ (v, r, s) = vm.sign(pks[i], _txHash);
+
+ signer = ecrecover(_txHash, v, r, s);
+
+ walletSigs[signer] = bytes.concat(r, s, bytes1(v));
+ signers[i] = uint256(uint160(signer));
+ }
+ _sort(signers, 0, int256(_signerCount - 1));
+
+ for (uint256 i = 0; i < _signerCount; ++i) {
+ address addy = address(uint160(signers[i]));
+ // emit log_address(addy);
+ signatures = bytes.concat(signatures, walletSigs[addy]);
+ }
+ }
+
+ function _signaturesForEthTransferTx(address _to, uint256 _value, uint256 _signerCount, ISafe _safe)
+ internal
+ returns (bytes memory signatures)
+ {
+ // create tx to send some eth from safe to wherever
+ bytes32 txHash = _getEthTransferSafeTxHash(_to, _value, _safe);
+ // have each signer sign the tx
+ // bytes[] memory sigs = new bytes[](signerCount);
+ uint8 v;
+ bytes32 r;
+ bytes32 s;
+ address signer;
+ uint256[] memory signers = new uint256[](_signerCount);
+
+ for (uint256 i = 0; i < _signerCount; ++i) {
+ // sign txHash
+ (v, r, s) = vm.sign(pks[i], txHash);
+
+ signer = ecrecover(txHash, v, r, s);
+
+ walletSigs[signer] = bytes.concat(r, s, bytes1(v));
+ signers[i] = uint256(uint160(signer));
+ // assert that the derived address matches what we have already stored
+ assertEq(address(uint160(signers[i])), signerAddresses[i], "signer address should match");
+ }
+
+ // sort the signers to match what Safe expects
+ _sort(signers, 0, int256(_signerCount - 1));
+
+ // concat the signatures in the order that Safe expects
+ for (uint256 i = 0; i < _signerCount; ++i) {
+ address addr = address(uint160(signers[i]));
+ signatures = bytes.concat(signatures, walletSigs[addr]);
+ }
+ }
+
+ function _createAddressesFromPks(uint256 _count)
+ internal
+ pure
+ returns (uint256[] memory pks_, address[] memory signerAddresses_)
+ {
+ pks_ = new uint256[](_count);
+ signerAddresses_ = new address[](_count);
+
+ for (uint256 i = 0; i < _count; ++i) {
+ pks_[i] = 100 * (i + 1);
+ signerAddresses_[i] = vm.addr(pks_[i]);
+ }
+ }
+
+ // borrowed from https://gist.github.com/subhodi/b3b86cc13ad2636420963e692a4d896f
+ function _sort(uint256[] memory _arr, int256 _left, int256 _right) internal view {
+ int256 i = _left;
+ int256 j = _right;
+ if (i == j) return;
+ uint256 pivot = _arr[uint256(_left + (_right - _left) / 2)];
+ while (i <= j) {
+ while (_arr[uint256(i)] < pivot) ++i;
+ while (pivot < _arr[uint256(j)]) j--;
+ if (i <= j) {
+ (_arr[uint256(i)], _arr[uint256(j)]) = (_arr[uint256(j)], _arr[uint256(i)]);
+ ++i;
+ j--;
+ }
+ }
+ if (_left < j) _sort(_arr, _left, j);
+ if (i < _right) _sort(_arr, i, _right);
+ }
+
+ function _findPrevOwner(address[] memory _owners, address _owner) internal pure returns (address prevOwner) {
+ prevOwner = SENTINELS;
+
+ for (uint256 i; i < _owners.length; ++i) {
+ if (_owners[i] == _owner) {
+ if (i == 0) break;
+ prevOwner = _owners[i - 1];
+ }
+ }
+ }
+
+ // borrowed from Orca (https://github.com/orcaprotocol/contracts/blob/main/contracts/utils/SafeTxHelper.sol)
+ function _getSafeTxHash(address _to, bytes memory _data, ISafe _safe) public view returns (bytes32 txHash) {
+ return _safe.getTransactionHash(
+ _to,
+ 0,
+ _data,
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ _safe.nonce()
+ );
+ }
+
+ function _getSafeDelegatecallHash(address _to, bytes memory _data, ISafe _safe)
+ internal
+ view
+ returns (bytes32 txHash)
+ {
+ return _safe.getTransactionHash(
+ _to, 0, _data, Enum.Operation.DelegateCall, 0, 0, 0, address(0), payable(address(0)), _safe.nonce()
+ );
+ }
+
+ // modified from Orca (https://github.com/orcaprotocol/contracts/blob/main/contracts/utils/SafeTxHelper.sol)
+ function _executeSafeTxFrom(address _from, bytes memory _data, ISafe _safe) public {
+ _safe.execTransaction(
+ address(_safe),
+ 0,
+ _data,
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ // (r,s,v) [r - from] [s - unused] [v - 1 flag for onchain approval]
+ abi.encode(_from, bytes32(0), bytes1(0x01))
+ );
+ }
+
+ function _executeEthTransferFromSafe(address _to, uint256 _value, uint256 _signerCount, ISafe _safe) public {
+ bytes32 txHash = _getEthTransferSafeTxHash(_to, _value, _safe);
+
+ bytes memory signatures = _createNSigsForTx(txHash, _signerCount);
+
+ _safe.execTransaction(
+ address(_safe),
+ _value,
+ "",
+ Enum.Operation.Call,
+ // not using the refunder
+ 0,
+ 0,
+ 0,
+ address(0),
+ payable(address(0)),
+ signatures
+ );
+ }
+}
+
+contract TestSuite is SafeTestHelpers {
+ // Constants
+ uint256 public constant TEST_SALT_NONCE = 1;
+ bytes32 public constant TEST_SALT = bytes32(abi.encode(TEST_SALT_NONCE));
+ bytes32 public constant GUARD_STORAGE_SLOT = 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8;
+
+ // Test environment
+ uint256 public FORK_BLOCK = 20_786_857;
+ string public chain = "mainnet";
+
+ // Test addresses
+ address public org = makeAddr("org");
+ address public owner = makeAddr("owner");
+ address public eligibility = makeAddr("eligibility");
+ address public toggle = makeAddr("toggle");
+ address public other = makeAddr("other");
+ address[] public fuzzingAddresses;
+
+ // Test delegatecall targets
+ address[] public defaultDelegatecallTargets;
+ address public v1_3_0_callOnly_canonical = 0x40A2aCCbd92BCA938b02010E17A5b8929b49130D;
+ address public v1_3_0_callOnly_eip155 = 0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B;
+ address public v1_4_1_callOnly_canonical = 0x9641d764fc13c8B624c04430C7356C1C7C8102e2;
+
+ // Test hats
+ uint256 public tophat;
+ uint256 public ownerHat;
+ uint256[] public signerHats;
+ uint256 public signerHat;
+
+ // Dependency contract addresses
+ IHats public hats;
+ ISafe public singletonSafe;
+ SafeProxyFactory public safeFactory;
+ ModuleProxyFactory public zodiacModuleFactory;
+ ISafe public safe;
+ address public safeFallbackLibrary;
+ address public safeMultisendLibrary;
+ address public caller;
+
+ // Contracts under test
+ HatsSignerGate public implementationHSG;
+ HatsSignerGate public instance;
+
+ // Test params
+ IHatsSignerGate.ThresholdConfig public thresholdConfig;
+ bool public locked;
+ TestGuard[] public tstGuards;
+ TestGuard public tstGuard;
+ address[] public tstModules;
+ address public tstModule1 = makeAddr("tstModule1");
+ address public tstModule2 = makeAddr("tstModule2");
+ address public tstModule3 = makeAddr("tstModule3");
+
+ // Utility variables
+ address[] initSafeOwners = new address[](1);
+
+ function setUp() public virtual {
+ // Set up the test environment with a fork
+ vm.createSelectFork(chain, FORK_BLOCK);
+
+ // Deploy the HSG implementation with a salt
+ DeployImplementation implementationDeployer = new DeployImplementation();
+ implementationDeployer.prepare(false);
+ implementationHSG = implementationDeployer.run();
+
+ // Cache the deploy params and factory address
+ safeFallbackLibrary = implementationDeployer.safeFallbackLibrary();
+ safeMultisendLibrary = implementationDeployer.safeMultisendLibrary();
+ safeFactory = SafeProxyFactory(implementationDeployer.safeProxyFactory());
+ zodiacModuleFactory = ModuleProxyFactory(implementationDeployer.zodiacModuleFactory());
+ singletonSafe = ISafe(payable(implementationDeployer.safeSingleton()));
+ hats = IHats(implementationDeployer.hats());
+
+ // Create test signer addresses
+ (pks, signerAddresses) = _createAddressesFromPks(20);
+
+ // generate fuzzing addresses
+ fuzzingAddresses = _generateFuzzingAddresses(50);
+
+ // create several test guards
+ tstGuard = new TestGuard{ salt: bytes32(0) }(address(instance));
+ tstGuards = new TestGuard[](3);
+ tstGuards[0] = tstGuard;
+ tstGuards[1] = new TestGuard{ salt: keccak256(abi.encode(1)) }(address(instance));
+ tstGuards[2] = new TestGuard{ salt: keccak256(abi.encode(2)) }(address(instance));
+
+ // set up the test modules array
+ tstModules = new address[](3);
+ tstModules[0] = tstModule1;
+ tstModules[1] = tstModule2;
+ tstModules[2] = tstModule3;
+
+ // set up the default delegatecall targets array
+ defaultDelegatecallTargets = new address[](3);
+ defaultDelegatecallTargets[0] = v1_3_0_callOnly_canonical;
+ defaultDelegatecallTargets[1] = v1_3_0_callOnly_eip155;
+ defaultDelegatecallTargets[2] = v1_4_1_callOnly_canonical;
+
+ // Set up the test hats
+ uint256 signerHatCount = 10;
+ signerHats = new uint256[](signerHatCount);
+
+ vm.startPrank(org);
+ tophat = hats.mintTopHat(org, "tophat", "https://hats.com");
+ ownerHat = hats.createHat(tophat, "owner", 500, eligibility, toggle, true, "");
+
+ for (uint256 i = 0; i < signerHatCount; ++i) {
+ signerHats[i] =
+ hats.createHat(tophat, string.concat("signerHat", vm.toString(i)), 500, eligibility, toggle, true, "image");
+ }
+
+ hats.mintHat(ownerHat, owner);
+ vm.stopPrank();
+
+ signerHat = signerHats[0];
+
+ // Set default test HSG params
+ thresholdConfig = IHatsSignerGate.ThresholdConfig({
+ thresholdType: IHatsSignerGate.TargetThresholdType.ABSOLUTE,
+ min: 2,
+ target: 2
+ });
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ DEPLOYMENT HELPERS
+ //////////////////////////////////////////////////////////////*/
+
+ function _deploySafe(address[] memory _owners, uint256 _threshold, uint256 _saltNonce) internal returns (ISafe) {
+ // encode safe setup parameters
+ bytes memory params = abi.encodeWithSignature(
+ "setup(address[],uint256,address,bytes,address,address,uint256,address)",
+ _owners,
+ _threshold,
+ address(0), // to
+ 0x0, // data
+ address(0), // fallback handler
+ address(0), // payment token
+ 0, // payment
+ address(0) // payment receiver
+ );
+
+ // deploy proxy of singleton from factory
+ return ISafe(payable(safeFactory.createProxyWithNonce(address(singletonSafe), params, _saltNonce)));
+ }
+
+ function _deployHSG(
+ uint256 _ownerHat,
+ uint256[] memory _signerHats,
+ IHatsSignerGate.ThresholdConfig memory _thresholdConfig,
+ address _safe,
+ bool _locked,
+ bool _claimableFor,
+ address _hsgGuard,
+ address[] memory _hsgModules,
+ bytes4 _expectedError,
+ bool _verbose
+ ) internal returns (HatsSignerGate) {
+ // create the instance deployer
+ DeployInstance instanceDeployer = new DeployInstance();
+ instanceDeployer.prepare1(
+ address(implementationHSG),
+ _ownerHat,
+ _signerHats,
+ _thresholdConfig,
+ _safe,
+ _locked,
+ _claimableFor,
+ _hsgGuard,
+ _hsgModules
+ );
+ instanceDeployer.prepare2(_verbose, TEST_SALT_NONCE);
+
+ if (_expectedError > 0) {
+ vm.expectRevert(_expectedError);
+ }
+
+ // deploy the instance
+ return instanceDeployer.run();
+ }
+
+ function _deployHSGAndSafe(
+ uint256 _ownerHat,
+ uint256[] memory _signerHats,
+ IHatsSignerGate.ThresholdConfig memory _thresholdConfig,
+ bool _locked,
+ bool _verbose,
+ bool _claimableFor,
+ address _hsgGuard,
+ address[] memory _hsgModules
+ ) internal returns (HatsSignerGate _instance, ISafe _safe) {
+ // create the instance deployer
+ DeployInstance instanceDeployer = new DeployInstance();
+ instanceDeployer.prepare1(
+ address(implementationHSG),
+ _ownerHat,
+ _signerHats,
+ _thresholdConfig,
+ address(0),
+ _locked,
+ _claimableFor,
+ _hsgGuard,
+ _hsgModules
+ );
+ instanceDeployer.prepare2(_verbose, TEST_SALT_NONCE);
+ _instance = instanceDeployer.run();
+ _safe = _instance.safe();
+ }
+
+ function _getSafeGuard(address _safe) internal view returns (address) {
+ return abi.decode(StorageAccessible(_safe).getStorageAt(uint256(GUARD_STORAGE_SLOT), 1), (address));
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ SIGNER SETTING HELPERS
+ //////////////////////////////////////////////////////////////*/
+
+ function _addSignersSameHat(uint256 _count, uint256 _hat) internal virtual {
+ for (uint256 i = 0; i < _count; ++i) {
+ _setSignerValidity(signerAddresses[i], _hat, true);
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(_hat, signerAddresses[i]);
+ vm.prank(signerAddresses[i]);
+ instance.claimSigner(_hat);
+ }
+ }
+
+ function _addSignersDifferentHats(uint256 _count, uint256[] memory _hats) internal {
+ for (uint256 i = 0; i < _count; ++i) {
+ _setSignerValidity(signerAddresses[i], _hats[i], true);
+ vm.prank(signerAddresses[i]);
+ instance.claimSigner(_hats[i]);
+ }
+ }
+
+ function _setSignerValidity(address _wearer, uint256 _hat, bool _result) internal {
+ if (_result) {
+ if (hats.isWearerOfHat(_wearer, _hat)) return;
+ // mint the hat to the wearer
+ vm.prank(org);
+ hats.mintHat(_hat, _wearer);
+ } else {
+ // revoke the wearer's hat
+ vm.prank(eligibility);
+ hats.setHatWearerStatus(_hat, _wearer, false, true);
+ }
+ }
+
+ /// @dev Construct the call and txHash for a single action multisend
+ function _constructSingleActionMultiSendTx(bytes memory _data)
+ internal
+ view
+ returns (bytes memory call, bytes32 txHash)
+ {
+ bytes memory multisendData = abi.encodePacked(
+ Enum.Operation.Call, // 0 for call; 1 for delegatecall
+ address(safe), // to
+ uint256(0), // value
+ uint256(_data.length), // data length
+ _data // data
+ );
+ call = abi.encodeWithSelector(MultiSend.multiSend.selector, multisendData);
+ txHash = _getTxHash(defaultDelegatecallTargets[0], 0, Enum.Operation.DelegateCall, call, safe);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ FUZZING HELPER FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ function _generateFuzzingAddresses(uint256 _count) internal returns (address[] memory) {
+ address[] memory addresses = new address[](_count);
+ for (uint256 i = 0; i < addresses.length; i++) {
+ addresses[i] = makeAddr(string.concat("fuzzing-", vm.toString(i)));
+ }
+ return addresses;
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ CUSTOM ASSERTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ function assertValidSignerHats(HatsSignerGate _instance, uint256[] memory _signerHats) public view {
+ for (uint256 i = 0; i < _signerHats.length; ++i) {
+ assertTrue(_instance.isValidSignerHat(_signerHats[i]));
+ }
+ }
+
+ function assertCorrectModules(HatsSignerGate _instance, address[] memory _modules) public view {
+ (address[] memory pagedModules, address next) = _instance.getModulesPaginated(SENTINELS, _modules.length);
+ assertEq(pagedModules.length, _modules.length);
+ for (uint256 i; i < _modules.length; ++i) {
+ // getModulesPaginated returns the modules in the reverse order they were added
+ assertEq(_modules[i], pagedModules[_modules.length - i - 1]);
+ }
+ assertEq(next, SENTINELS);
+ }
+
+ function assertEq(IHatsSignerGate.ThresholdConfig memory _actual, IHatsSignerGate.ThresholdConfig memory _expected)
+ public
+ pure
+ {
+ assertEq(uint8(_actual.thresholdType), uint8(_expected.thresholdType), "incorrect threshold type");
+ assertEq(_actual.min, _expected.min, "incorrect min");
+ assertEq(_actual.target, _expected.target, "incorrect target");
+ }
+
+ function assertOnlyModule(ISafe _safe, address _module) public view {
+ (address[] memory modules, address next) = _safe.getModulesPaginated(SENTINELS, 1);
+ assertEq(modules.length, 1, "should only have one module");
+ assertEq(modules[0], _module, "module should be the only module");
+ assertEq(next, SENTINELS, "next should be SENTINELS");
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ THRESHOLD CONFIG HELPER FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ function _createValidThresholdConfig(
+ IHatsSignerGate.TargetThresholdType _thresholdType,
+ uint8 _min, // keep values at least somewhat realistic
+ uint16 _target // keep values at least somewhat realistic
+ ) internal pure returns (IHatsSignerGate.ThresholdConfig memory) {
+ // ensure the min is at least 1
+ uint120 min = uint120(bound(_min, 1, type(uint8).max));
+
+ uint120 target;
+ if (_thresholdType == IHatsSignerGate.TargetThresholdType.ABSOLUTE) {
+ // ensure the target is at least the min
+ target = uint120(bound(_target, min, type(uint16).max));
+ } else {
+ // ensure the target is no bigger than 100% (10000)
+ target = uint120(bound(_target, 1, 10_000));
+ }
+
+ console2.log("config.thresholdType", uint8(_thresholdType));
+ console2.log("config.min", min);
+ console2.log("config.target", target);
+
+ return IHatsSignerGate.ThresholdConfig({ thresholdType: _thresholdType, min: min, target: target });
+ }
+
+ function _calcProportionalTargetSignatures(uint256 _ownerCount, uint120 _target) internal pure returns (uint256) {
+ return ((_ownerCount * _target) + 9999) / 10_000;
+ }
+
+ /// @dev Assumes _min and _target are valid
+ function _calcProportionalRequiredValidSignatures(uint256 _ownerCount, uint120 _min, uint120 _target)
+ internal
+ pure
+ returns (uint256)
+ {
+ if (_ownerCount < _min) return _min;
+ uint256 required = _calcProportionalTargetSignatures(_ownerCount, _target);
+ if (required < _min) return _min;
+ return required;
+ }
+
+ function _calcAbsoluteRequiredValidSignatures(uint256 _ownerCount, uint120 _min, uint120 _target)
+ internal
+ pure
+ returns (uint256)
+ {
+ if (_ownerCount < _min) return _min;
+ if (_ownerCount > _target) return _target;
+ return _ownerCount;
+ }
+
+ function _calcRequiredValidSignatures(uint256 _ownerCount, IHatsSignerGate.ThresholdConfig memory _config)
+ internal
+ pure
+ returns (uint256)
+ {
+ if (_config.thresholdType == IHatsSignerGate.TargetThresholdType.ABSOLUTE) {
+ return _calcAbsoluteRequiredValidSignatures(_ownerCount, _config.min, _config.target);
+ }
+ return _calcProportionalRequiredValidSignatures(_ownerCount, _config.min, _config.target);
+ }
+
+ /// @dev Mocks the `isWearerOfHat` function for a given wearer and hat. Useful when testing with hat ids that are not
+ /// necessarily real hats.
+ function _mockHatWearer(address _wearer, uint256 _hatId, bool _isWearer) internal {
+ vm.mockCall(
+ address(hats), abi.encodeWithSelector(hats.isWearerOfHat.selector, _wearer, _hatId), abi.encode(_isWearer)
+ );
+ }
+
+ function _getRandomBool(uint256 _seed) internal pure returns (bool) {
+ return _seed % 2 == 0;
+ }
+
+ /// @dev Returns a `_count` of random signer hats, that are not necessarily real hat ids
+ function _getRandomSignerHats(uint256 _count) internal returns (uint256[] memory) {
+ // Bound number of hats to a semi-reasonable range
+ uint256 numHats = bound(_count, 1, 50);
+
+ // Create array of signer hats
+ uint256[] memory signerHats_ = new uint256[](numHats);
+ for (uint256 i; i < numHats; i++) {
+ signerHats_[i] = uint256(keccak256(abi.encode(vm.randomUint(), "hat", i)));
+ }
+ return signerHats_;
+ }
+
+ /// @dev Returns a `_count` of randomly selected valid signer hats, sampled without replacement
+ function _getRandomValidSignerHatsWithoutReplacement(uint256 _seed, uint256 _count)
+ internal
+ view
+ returns (uint256[] memory)
+ {
+ _count = bound(_count, 1, signerHats.length);
+ uint256[] memory selected = new uint256[](_count);
+ bool[] memory used = new bool[](signerHats.length);
+ uint256 selectedCount;
+
+ while (selectedCount < _count) {
+ uint256 index = _seed % signerHats.length;
+ if (!used[index]) {
+ selected[selectedCount] = signerHats[index];
+ used[index] = true;
+ selectedCount++;
+ }
+ }
+
+ return selected;
+ }
+
+ /// @dev Returns a `_count` of randomly selected valid signer hats, sampled with replacement
+ function _getRandomValidSignerHatsWithReplacement(uint256 _seed, uint256 _count)
+ internal
+ view
+ returns (uint256[] memory)
+ {
+ _count = bound(_count, 1, signerHats.length);
+ uint256[] memory signerHats_ = new uint256[](_count);
+ for (uint256 i; i < _count; i++) {
+ signerHats_[i] = _getRandomValidSignerHat(_seed + i);
+ }
+ return signerHats_;
+ }
+
+ function _getRandomValidSignerHat(uint256 _seed) internal view returns (uint256) {
+ uint256 index = _seed % signerHats.length;
+ return signerHats[index];
+ }
+
+ function _getRandomAddress() internal returns (address) {
+ return _getRandomAddress(vm.randomUint());
+ }
+
+ function _getRandomAddress(uint256 _seed) internal view returns (address) {
+ uint256 addressIndex = _seed % fuzzingAddresses.length;
+ return fuzzingAddresses[addressIndex];
+ }
+
+ function _getRandomSigners(uint256 _seed, uint256 _count) internal view returns (address[] memory) {
+ _count = bound(_count, 1, signerAddresses.length);
+ address[] memory signers = new address[](_count);
+ bool[] memory used = new bool[](signerAddresses.length);
+ uint256 selectedCount;
+
+ while (selectedCount < _count) {
+ uint256 index = uint256(keccak256(abi.encode(_seed, selectedCount))) % signerAddresses.length;
+ if (!used[index]) {
+ signers[selectedCount] = signerAddresses[index];
+ used[index] = true;
+ selectedCount++;
+ }
+ }
+
+ return signers;
+ }
+
+ function _getRandomAddresses(uint256 _seed, uint256 _count) internal view returns (address[] memory) {
+ require(_count <= fuzzingAddresses.length, "Count exceeds available addresses");
+ address[] memory addresses = new address[](_count);
+ bool[] memory used = new bool[](fuzzingAddresses.length);
+ uint256 selectedCount;
+
+ while (selectedCount < _count) {
+ uint256 index = uint256(keccak256(abi.encode(_seed, selectedCount))) % fuzzingAddresses.length;
+ if (!used[index]) {
+ addresses[selectedCount] = fuzzingAddresses[index];
+ used[index] = true;
+ selectedCount++;
+ }
+ }
+
+ return addresses;
+ }
+
+ function _getRandomGuard(uint256 _seed) internal view returns (address) {
+ uint256 guardIndex = _seed % tstGuards.length;
+ return address(tstGuards[guardIndex]);
+ }
+
+ function _getSignaturesForEmptyTx(uint256 _signerCount, address _to, Enum.Operation _operation)
+ internal
+ returns (bytes memory)
+ {
+ // execute a transaction from the safe to set the nonce
+ bytes32 txHash;
+ if (_operation == Enum.Operation.Call) {
+ txHash = _getSafeTxHash(_to, "", safe);
+ } else {
+ txHash = _getSafeDelegatecallHash(_to, "", safe);
+ }
+ return _createNSigsForTx(txHash, _signerCount);
+ }
+
+ function _executeEmptyCallFromSafe(uint256 _signerCount, address _to) internal {
+ // add the signers
+ _addSignersSameHat(_signerCount, signerHat);
+
+ // execute a transaction from the safe to set the nonce
+ bytes memory signatures = _getSignaturesForEmptyTx(_signerCount, _to, Enum.Operation.Call);
+ safe.execTransaction(_to, 0, "", Enum.Operation.Call, 0, 0, 0, address(0), payable(address(0)), signatures);
+ }
+
+ function _createContractSignature(address _contractSigner) internal pure returns (bytes memory signature) {
+ // r encodes the address of the contract that signed the message
+ bytes32 r = bytes32(uint256(uint160(_contractSigner)));
+ // s can be 0
+ bytes32 s = bytes32(0);
+ // v must be 0
+ bytes1 v = bytes1(0);
+ return abi.encodePacked(r, s, v);
+ }
+
+ function _createNContractSigs(uint256 _signerCount) internal view returns (bytes memory signatures) {
+ for (uint256 i; i < _signerCount; ++i) {
+ signatures = bytes.concat(signatures, _createContractSignature(signerAddresses[i]));
+ }
+ return signatures;
+ }
+
+ modifier callerIsOwner(bool _isOwner) {
+ // randomly select a caller
+ caller = _getRandomAddress();
+
+ // mint them the owner hat
+ _setSignerValidity(caller, ownerHat, _isOwner);
+
+ _;
+ }
+
+ modifier callerIsSafe(bool _isSafe) {
+ if (_isSafe) {
+ caller = address(safe);
+ } else {
+ caller = _getRandomAddress();
+ }
+ _;
+ }
+}
+
+contract WithHSGInstanceTest is TestSuite {
+ function setUp() public virtual override {
+ super.setUp();
+
+ (instance, safe) = _deployHSGAndSafe({
+ _ownerHat: ownerHat,
+ _signerHats: signerHats,
+ _thresholdConfig: thresholdConfig,
+ _locked: false,
+ _claimableFor: false,
+ _hsgGuard: address(0), // no guard
+ _hsgModules: new address[](0), // no modules
+ _verbose: false
+ });
+ }
+
+ modifier isLocked(bool _locked) {
+ if (_locked && !instance.locked()) {
+ vm.prank(owner);
+ instance.lock();
+ }
+ _;
+ }
+
+ modifier isClaimableFor(bool _claimableFor) {
+ if (_claimableFor && !instance.claimableFor()) {
+ vm.prank(owner);
+ instance.setClaimableFor(true);
+ }
+ _;
+ }
+}
+
+contract WithHSGHarnessInstanceTest is TestSuite {
+ HatsSignerGateHarness public harnessImplementation;
+ HatsSignerGateHarness public harness;
+
+ IHatsSignerGate.SetupParams public harnessSetupParams;
+
+ function setUp() public virtual override {
+ super.setUp();
+
+ // deploy the harness implementation
+ harnessImplementation = new HatsSignerGateHarness(
+ address(hats),
+ address(singletonSafe),
+ address(safeFallbackLibrary),
+ address(safeMultisendLibrary),
+ address(safeFactory)
+ );
+
+ // set up the harness setup params
+ harnessSetupParams = IHatsSignerGate.SetupParams({
+ ownerHat: ownerHat,
+ signerHats: signerHats,
+ safe: address(0),
+ thresholdConfig: thresholdConfig,
+ locked: false,
+ claimableFor: false,
+ implementation: address(harnessImplementation),
+ hsgGuard: address(0),
+ hsgModules: new address[](0)
+ });
+
+ // deploy a harness instance
+ harness = HatsSignerGateHarness(
+ ModuleProxyFactory(zodiacModuleFactory).deployModule(
+ address(harnessImplementation),
+ abi.encodeWithSignature("setUp(bytes)", abi.encode(harnessSetupParams)),
+ TEST_SALT_NONCE
+ )
+ );
+
+ safe = harness.safe();
+ }
+
+ function _addSignersSameHat(uint256 _count, uint256 _hat) internal override {
+ for (uint256 i = 0; i < _count; ++i) {
+ _setSignerValidity(signerAddresses[i], _hat, true);
+ vm.expectEmit();
+ emit IHatsSignerGate.Registered(_hat, signerAddresses[i]);
+ vm.prank(signerAddresses[i]);
+ harness.claimSigner(_hat);
+ }
+ }
+
+ /// @dev Adds a random number of non-duplicate signers to the safe, randomly selected from the fuzzing addresses
+ function _addRandomSigners(uint8 _numExistingSigners) internal {
+ // Ensure we have at least one existing signer
+ _numExistingSigners = uint8(bound(_numExistingSigners, 1, fuzzingAddresses.length - 1));
+
+ // Use the random seed to generate multiple indices
+ uint256[] memory usedIndices = new uint256[](_numExistingSigners);
+ for (uint256 i; i < _numExistingSigners; i++) {
+ // Generate a new index from the random seed
+ uint256 index = uint256(keccak256(abi.encode(vm.randomUint(), i))) % fuzzingAddresses.length;
+
+ // Ensure no duplicates
+ bool isDuplicate;
+ for (uint256 j; j < i; j++) {
+ if (usedIndices[j] == index) {
+ isDuplicate = true;
+ break;
+ }
+ }
+ if (!isDuplicate) {
+ usedIndices[i] = index;
+
+ // Add the signer
+ address signer = fuzzingAddresses[index];
+ harness.exposed_addSigner(signer);
+
+ assertTrue(safe.isOwner(signer), "signer should be added to the safe");
+ assertFalse(safe.isOwner(address(harness)), "the harness should no longer be an owner");
+
+ // Ensure the threshold is correct
+ uint256 correctThreshold = harness.exposed_getNewThreshold(safe.getOwners().length);
+ assertEq(safe.getThreshold(), correctThreshold, "the safe threshold should be correct");
+ }
+ }
+ }
+
+ /// @dev Helper function to generate unique signatures and track valid signers.
+ /// @param _dataHash The hash to sign
+ /// @param _sigCount The number of signatures to generate
+ /// @param _ethSign Whether to use eth_sign
+ /// @return signatures The concatenated signatures bytes
+ /// @return validCount The number of valid signers
+ function _generateUniqueECDSASignatures(
+ bytes32 _dataHash,
+ uint256 _sigCount,
+ bool _ethSign,
+ HatsSignerGateHarness _harness
+ ) internal returns (bytes memory signatures, uint256 validCount) {
+ signatures = new bytes(0);
+ address[] memory signers = new address[](_sigCount);
+ bool[] memory used = new bool[](signerAddresses.length);
+
+ for (uint256 i; i < _sigCount; i++) {
+ // Generate random index for selecting unused signer
+ uint256 signerIndex;
+ do {
+ signerIndex = uint256(keccak256(abi.encode(vm.randomUint(), i))) % signerAddresses.length;
+ } while (used[signerIndex]);
+
+ // Mark this signer as used
+ used[signerIndex] = true;
+
+ // if ethSign is true, use the eth_sign prefix
+ bytes32 dataHash =
+ _ethSign ? keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _dataHash)) : _dataHash;
+
+ // create a signature for the data hash from the selected signer
+ (uint8 v, bytes32 r, bytes32 s) = vm.sign(pks[signerIndex], dataHash);
+
+ // if ethSign is true, adjust v for the eth_sign prefix
+ v = _ethSign ? v + 4 : v;
+
+ // concatenate the components into a single bytes array
+ bytes memory signature = abi.encodePacked(r, s, bytes1(v));
+ assertEq(signature.length, 65, "signature length should be 65");
+
+ // add the signature to the signatures array
+ signatures = bytes.concat(signatures, signature);
+ assertEq(signatures.length, (i + 1) * 65, "signatures length should 65 * number of sigs");
+
+ // add the signer to the signers array
+ signers[i] = signerAddresses[signerIndex];
+
+ // Set validity and track expected count
+ bool isValid = _getRandomBool(i);
+ _setSignerValidity(signers[i], signerHat, isValid);
+ if (isValid) {
+ _harness.exposed_registerSigner(signerHat, signers[i], false);
+ validCount++;
+ }
+ }
+ }
+
+ /// @dev Helper function to generate unique non-ECDSA signatures and track valid signers.
+ /// @param _sigCount The number of signatures to generate
+ /// @param _approvedHash Whether to use approved hash signatures (true) or contract signatures (false)
+ /// @return signatures The concatenated signatures bytes
+ /// @return validCount The number of valid signers
+ function _generateUniqueNonECDSASignatures(uint256 _sigCount, bool _approvedHash, HatsSignerGateHarness _harness)
+ internal
+ returns (bytes memory signatures, uint256 validCount)
+ {
+ signatures = new bytes(0);
+ address[] memory signers = new address[](_sigCount);
+ bool[] memory used = new bool[](signerAddresses.length);
+
+ for (uint256 i; i < _sigCount; i++) {
+ // Generate random index for selecting unused signer
+ uint256 signerIndex;
+ do {
+ signerIndex = uint256(keccak256(abi.encode(vm.randomUint(), i))) % signerAddresses.length;
+ } while (used[signerIndex]);
+
+ // Mark this signer as used
+ used[signerIndex] = true;
+
+ // encode the signer address into r
+ bytes32 r = bytes32(uint256(uint160(signerAddresses[signerIndex])));
+ bytes32 s = bytes32(0);
+
+ // set v based on whether we are using approved hash signatures (v=1) or contract signatures (v=0)
+ uint8 v = _approvedHash ? 1 : 0;
+
+ // concatenate the components into a single bytes array
+ bytes memory signature = abi.encodePacked(r, s, bytes1(v));
+ assertEq(signature.length, 65, "signature length should be 65");
+
+ // add the signature to the signatures array
+ signatures = bytes.concat(signatures, signature);
+ assertEq(signatures.length, (i + 1) * 65, "signatures length should 65 * number of sigs");
+
+ // add the signer to the signers array
+ signers[i] = signerAddresses[signerIndex];
+
+ // Set validity and track expected count
+ bool isValid = _getRandomBool(i);
+ _setSignerValidity(signers[i], signerHat, isValid);
+ if (isValid) {
+ _harness.exposed_registerSigner(signerHat, signers[i], false);
+ validCount++;
+ }
+ }
+ }
+
+ function _assertTransientStateVariables(
+ Enum.Operation _operation,
+ bytes32 _existingOwnersHash,
+ uint256 _existingThreshold,
+ address _existingFallbackHandler,
+ bool _inSafeExecTransaction,
+ bool _inModuleExecTransaction,
+ uint256 _initialNonce,
+ uint256 _checkTransactionCounter
+ ) internal view {
+ {
+ assertEq(uint8(harness.operation()), uint8(_operation), "operation should be set");
+ assertEq(harness.inSafeExecTransaction(), _inSafeExecTransaction, "inSafeExecTransaction should be set");
+ assertEq(harness.inModuleExecTransaction(), _inModuleExecTransaction, "inModuleExecTransaction should be set");
+ assertEq(harness.initialNonce(), _initialNonce, "initial nonce should be set");
+ assertEq(harness.checkTransactionCounter(), _checkTransactionCounter, "checkTransactionCounter should be set");
+ if (_operation == Enum.Operation.DelegateCall) {
+ assertEq(harness.existingOwnersHash(), _existingOwnersHash, "existing owners hash should be set");
+ assertEq(harness.existingThreshold(), _existingThreshold, "existing threshold should be set");
+ assertEq(harness.existingFallbackHandler(), _existingFallbackHandler, "existing fallback handler should be set");
+ } else {
+ assertEq(harness.existingOwnersHash(), bytes32(0), "existing owners hash should be empty");
+ assertEq(harness.existingThreshold(), 0, "existing threshold should be empty");
+ assertEq(harness.existingFallbackHandler(), address(0), "existing fallback handler should be empty");
+ }
+ }
+ }
+
+ /// @dev Gets the existing state stored in transient storage by `_checkModuleTransaction` and asserts it matches
+ /// the provided values
+ function assertCorrectTransientState(
+ bytes32 _existingOwnersHash,
+ uint256 _existingThreshold,
+ address _existingFallbackHandler
+ ) internal view {
+ assertEq(harness.exposed_existingOwnersHash(), _existingOwnersHash, "the existing owners hash should be unchanged");
+ assertEq(harness.exposed_existingThreshold(), _existingThreshold, "the existing threshold should be unchanged");
+ assertEq(
+ harness.exposed_existingFallbackHandler(),
+ _existingFallbackHandler,
+ "the existing fallback handler should be unchanged"
+ );
+ }
+
+ /// @dev Forces the value of the `_inSafeExecTransaction` transient variable
+ modifier inSafeExecTransaction(bool _inSafeExecTransaction) {
+ harness.setInSafeExecTransaction(_inSafeExecTransaction);
+ _;
+ }
+
+ /// @dev Forces the value of the `_inModuleExecTransaction` transient variable
+ modifier inModuleExecTransaction(bool _inModuleExecTransaction) {
+ harness.setInModuleExecTransaction(_inModuleExecTransaction);
+ _;
+ }
+}
diff --git a/test/harnesses/HatsSignerGateHarness.sol b/test/harnesses/HatsSignerGateHarness.sol
new file mode 100644
index 0000000..d5134f3
--- /dev/null
+++ b/test/harnesses/HatsSignerGateHarness.sol
@@ -0,0 +1,210 @@
+// SPDX-License-Identifier: LGPL-3.0
+pragma solidity >=0.8.13;
+
+// import { Test, console2 } from "../../lib/forge-std/src/Test.sol"; // comment out after testing
+import { IHats } from "../../lib/hats-protocol/src/Interfaces/IHats.sol";
+import { HatsSignerGate } from "../../src/HatsSignerGate.sol";
+import { SafeManagerLibHarness } from "./SafeManagerLibHarness.sol";
+import { IHatsSignerGate } from "../../src/interfaces/IHatsSignerGate.sol";
+import { BaseGuard, IGuard } from "../../lib/zodiac/contracts/guard/BaseGuard.sol";
+import { GuardableUnowned } from "../../src/lib/zodiac-modified/GuardableUnowned.sol";
+import { ModifierUnowned } from "../../src/lib/zodiac-modified/ModifierUnowned.sol";
+import { Multicallable } from "../../lib/solady/src/utils/Multicallable.sol";
+import { SignatureDecoder } from "../../lib/safe-smart-account/contracts/common/SignatureDecoder.sol";
+import { ISafe, Enum } from "../../src/lib/safe-interfaces/ISafe.sol";
+
+/// @dev A harness for testing HatsSignerGate internal functions
+contract HatsSignerGateHarness is HatsSignerGate, SafeManagerLibHarness {
+ constructor(
+ address _hats,
+ address _safeSingleton,
+ address _safeFallbackLibrary,
+ address _safeMultisendLibrary,
+ address _safeProxyFactory
+ ) HatsSignerGate(_hats, _safeSingleton, _safeFallbackLibrary, _safeMultisendLibrary, _safeProxyFactory) { }
+
+ /*//////////////////////////////////////////////////////////////
+ EXPOSED TRANSIENT STATE
+ //////////////////////////////////////////////////////////////*/
+
+ bytes32 public existingOwnersHash;
+ uint256 public existingThreshold;
+ address public existingFallbackHandler;
+ Enum.Operation public operation;
+ bool public inModuleExecTransaction;
+ bool public inSafeExecTransaction;
+ uint256 public initialNonce;
+ uint256 public checkTransactionCounter;
+
+ /*//////////////////////////////////////////////////////////////
+ TRANSIENT STATE SETTERS
+ //////////////////////////////////////////////////////////////*/
+
+ function setExistingOwnersHash(bytes32 existingOwnersHash_) public {
+ _existingOwnersHash = existingOwnersHash_;
+ }
+
+ function setExistingThreshold(uint256 existingThreshold_) public {
+ _existingThreshold = existingThreshold_;
+ }
+
+ function setExistingFallbackHandler(address existingFallbackHandler_) public {
+ _existingFallbackHandler = existingFallbackHandler_;
+ }
+
+ function setInModuleExecTransaction(bool inModuleExecTransaction_) public {
+ _inModuleExecTransaction = inModuleExecTransaction_;
+ }
+
+ function setInSafeExecTransaction(bool inSafeExecTransaction_) public {
+ _inSafeExecTransaction = inSafeExecTransaction_;
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ EXPOSED INTERNAL FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ function exposed_checkOwner() public view {
+ _checkOwner();
+ }
+
+ function exposed_checkUnlocked() public view {
+ _checkUnlocked();
+ }
+
+ function exposed_lock() public {
+ _lock();
+ }
+
+ function exposed_setDelegatecallTarget(address _target, bool _enabled) public {
+ _setDelegatecallTarget(_target, _enabled);
+ }
+
+ function exposed_setClaimableFor(bool _claimableFor) public {
+ _setClaimableFor(_claimableFor);
+ }
+
+ function exposed_registerSigner(uint256 _hatId, address _signer, bool _allowReregistration) public {
+ _registerSigner(_hatId, _signer, _allowReregistration);
+ }
+
+ function exposed_addSigner(address _signer) public {
+ _addSigner(_signer);
+ }
+
+ function exposed_removeSigner(address _signer) public {
+ _removeSigner(_signer);
+ }
+
+ function exposed_setOwnerHat(uint256 _ownerHat) public {
+ _setOwnerHat(_ownerHat);
+ }
+
+ function exposed_addSignerHats(uint256[] memory _newSignerHats) public {
+ _addSignerHats(_newSignerHats);
+ }
+
+ function exposed_setThresholdConfig(ThresholdConfig memory _config) public {
+ _setThresholdConfig(_config);
+ }
+
+ function exposed_countValidSigners(address[] memory owners) public view returns (uint256) {
+ return _countValidSigners(owners);
+ }
+
+ function exposed_countValidSignatures(bytes32 dataHash, bytes memory signatures, uint256 sigCount)
+ public
+ view
+ returns (uint256)
+ {
+ return _countValidSignatures(dataHash, signatures, sigCount);
+ }
+
+ function exposed_checkModuleTransaction(address _to, Enum.Operation operation_, ISafe _safe) public {
+ _checkModuleTransaction(_to, operation_, _safe);
+ }
+
+ function exposed_checkSafeState(ISafe _safe) public view {
+ _checkSafeState(_safe);
+ }
+
+ function exposed_enableModule(address module) public {
+ _enableModule(module);
+ }
+
+ function exposed_setGuard(address _guard) public {
+ _setGuard(_guard);
+ }
+
+ function exposed_getRequiredValidSignatures(uint256 numOwners) public view returns (uint256) {
+ return _getRequiredValidSignatures(numOwners);
+ }
+
+ function exposed_getNewThreshold(uint256 numOwners) public view returns (uint256) {
+ return _getNewThreshold(numOwners);
+ }
+
+ function exposed_existingOwnersHash() public view returns (bytes32) {
+ return _existingOwnersHash;
+ }
+
+ function exposed_existingThreshold() public view returns (uint256) {
+ return _existingThreshold;
+ }
+
+ function exposed_existingFallbackHandler() public view returns (address) {
+ return _existingFallbackHandler;
+ }
+
+ function exposed_operation() public view returns (Enum.Operation) {
+ return _operation;
+ }
+
+ function exposed_inModuleExecTransaction() public view returns (bool) {
+ return _inModuleExecTransaction;
+ }
+
+ function exposed_inSafeExecTransaction() public view returns (bool) {
+ return _inSafeExecTransaction;
+ }
+
+ function exposed_initialNonce() public view returns (uint256) {
+ return _initialNonce;
+ }
+
+ function exposed_checkTransactionCounter() public view returns (uint256) {
+ return _checkTransactionCounter;
+ }
+
+ /// @dev Exposes the transient state variables set within checkTransaction
+ function exposed_checkTransaction(
+ address to,
+ uint256 value,
+ bytes memory data,
+ Enum.Operation op,
+ uint256 safeTxGas,
+ uint256 baseGas,
+ uint256 gasPrice,
+ address gasToken,
+ address payable refundReceiver,
+ bytes memory signatures,
+ address sender
+ ) public {
+ checkTransaction(to, value, data, op, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, signatures, sender);
+
+ // store the transient state in persistent storage for access in tests
+ operation = _operation;
+ existingOwnersHash = _existingOwnersHash;
+ existingThreshold = _existingThreshold;
+ existingFallbackHandler = _existingFallbackHandler;
+ inModuleExecTransaction = _inModuleExecTransaction;
+ inSafeExecTransaction = _inSafeExecTransaction;
+ initialNonce = _initialNonce;
+ checkTransactionCounter = _checkTransactionCounter;
+ }
+
+ /// @dev Allows tests to call checkAfterExecution by mocking the guardEntries transient state variable
+ function exposed_checkAfterExecution(bytes32 _txHash, bool _success) public {
+ checkAfterExecution(_txHash, _success);
+ }
+}
diff --git a/test/harnesses/SafeManagerLibHarness.sol b/test/harnesses/SafeManagerLibHarness.sol
new file mode 100644
index 0000000..eb96064
--- /dev/null
+++ b/test/harnesses/SafeManagerLibHarness.sol
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: LGPL-3.0
+pragma solidity >=0.8.13;
+
+// import { console2 } from "../lib/forge-std/src/console2.sol";
+import { SafeManagerLib } from "../../src/lib/SafeManagerLib.sol";
+import { MultiSend } from "../../lib/safe-smart-account/contracts/libraries/MultiSend.sol";
+import { SafeProxyFactory } from "../../lib/safe-smart-account/contracts/proxies/SafeProxyFactory.sol";
+import { StorageAccessible } from "../../lib/safe-smart-account/contracts/common/StorageAccessible.sol";
+import { Enum, ISafe, IGuardManager, IModuleManager, IOwnerManager } from "../../src/lib/safe-interfaces/ISafe.sol";
+import { IGuard } from "../../lib/zodiac/contracts/interfaces/IGuard.sol";
+
+/// @dev A harness for testing SafeManagerLib internal functions
+contract SafeManagerLibHarness {
+ function deploySafeAndAttachHSG(
+ address _safeProxyFactory,
+ address _safeSingleton,
+ address _safeFallbackLibrary,
+ address _safeMultisendLibrary
+ ) public returns (address payable) {
+ return SafeManagerLib.deploySafeAndAttachHSG(
+ _safeProxyFactory, _safeSingleton, _safeFallbackLibrary, _safeMultisendLibrary
+ );
+ }
+
+ function encodeEnableModuleAction(address _moduleToEnable) public pure returns (bytes memory) {
+ return SafeManagerLib.encodeEnableModuleAction(_moduleToEnable);
+ }
+
+ function encodeDisableModuleAction(address _previousModule, address _moduleToDisable)
+ public
+ pure
+ returns (bytes memory)
+ {
+ return SafeManagerLib.encodeDisableModuleAction(_previousModule, _moduleToDisable);
+ }
+
+ function encodeSetGuardAction(address _guard) public pure returns (bytes memory) {
+ return SafeManagerLib.encodeSetGuardAction(_guard);
+ }
+
+ function encodeRemoveHSGAsGuardAction() public pure returns (bytes memory) {
+ return SafeManagerLib.encodeRemoveHSGAsGuardAction();
+ }
+
+ function encodeSwapOwnerAction(address _prevOwner, address _oldOwner, address _newOwner)
+ public
+ pure
+ returns (bytes memory)
+ {
+ return SafeManagerLib.encodeSwapOwnerAction(_prevOwner, _oldOwner, _newOwner);
+ }
+
+ function encodeRemoveOwnerAction(address _prevOwner, address _oldOwner, uint256 _newThreshold)
+ public
+ pure
+ returns (bytes memory)
+ {
+ return SafeManagerLib.encodeRemoveOwnerAction(_prevOwner, _oldOwner, _newThreshold);
+ }
+
+ function encodeAddOwnerWithThresholdAction(address _owner, uint256 _newThreshold) public pure returns (bytes memory) {
+ return SafeManagerLib.encodeAddOwnerWithThresholdAction(_owner, _newThreshold);
+ }
+
+ function encodeChangeThresholdAction(uint256 _newThreshold) public pure returns (bytes memory) {
+ return SafeManagerLib.encodeChangeThresholdAction(_newThreshold);
+ }
+
+ function execSafeTransactionFromHSG(ISafe _safe, bytes memory _data) public {
+ SafeManagerLib.execSafeTransactionFromHSG(_safe, _data);
+ }
+
+ function execDisableHSGAsOnlyModule(ISafe _safe) public {
+ SafeManagerLib.execDisableHSGAsOnlyModule(_safe);
+ }
+
+ function execDisableHSGAsModule(ISafe _safe, address _previousModule) public {
+ SafeManagerLib.execDisableHSGAsModule(_safe, _previousModule);
+ }
+
+ function execRemoveHSGAsGuard(ISafe _safe) public {
+ SafeManagerLib.execRemoveHSGAsGuard(_safe);
+ }
+
+ function execAttachNewHSG(ISafe _safe, address _newHSG) public {
+ SafeManagerLib.execAttachNewHSG(_safe, _newHSG);
+ }
+
+ function execChangeThreshold(ISafe _safe, uint256 _newThreshold) public {
+ SafeManagerLib.execChangeThreshold(_safe, _newThreshold);
+ }
+
+ function getSafeGuard(ISafe _safe) public view returns (address) {
+ return SafeManagerLib.getSafeGuard(_safe);
+ }
+
+ function getSafeFallbackHandler(ISafe _safe) public view returns (address) {
+ return SafeManagerLib.getSafeFallbackHandler(_safe);
+ }
+
+ function getModulesWith1(ISafe _safe) public view returns (address[] memory modulesWith1, address next) {
+ return SafeManagerLib.getModulesWith1(_safe);
+ }
+
+ function canAttachHSG(ISafe _safe) public view returns (bool) {
+ return SafeManagerLib.canAttachHSG(_safe);
+ }
+
+ function findPrevOwner(address[] memory _owners, address _owner) public pure returns (address) {
+ return SafeManagerLib.findPrevOwner(_owners, _owner);
+ }
+}
diff --git a/test/mocks/TestGuard.sol b/test/mocks/TestGuard.sol
new file mode 100644
index 0000000..6302605
--- /dev/null
+++ b/test/mocks/TestGuard.sol
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: LGPL-3.0-only
+pragma solidity >=0.7.0 <0.9.0;
+
+import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
+
+import { BaseGuard } from "../../lib/zodiac/contracts/guard/BaseGuard.sol";
+import { Enum } from "../../lib/safe-smart-account/contracts/common/Enum.sol";
+
+/* solhint-disable */
+
+/// @notice Modified from Zodiac's TestGuard to allow for disallowing execution in checkAfterExecution
+/// https://github.com/gnosisguild/zodiac/blob/5165ce2f377c291d4bfe71d21948d9df0fdf6224/contracts/test/TestGuard.sol
+contract TestGuard is BaseGuard {
+ event PreChecked(address sender);
+ event PostChecked(bool checked);
+
+ address public module;
+ bool public executionDisallowed;
+
+ constructor(address _module) {
+ bytes memory initParams = abi.encode(_module);
+ setUp(initParams);
+ }
+
+ function setModule(address _module) public {
+ module = _module;
+ }
+
+ /// @dev Disallows execution by causing a revert in checkAfterExecution. Useful for testing checkAfterExecution.
+ function disallowExecution() public {
+ executionDisallowed = true;
+ }
+
+ /// @dev Modified to remove the operation restriction
+ function checkTransaction(
+ address to,
+ uint256 value,
+ bytes memory data,
+ Enum.Operation, /* operation */
+ uint256,
+ uint256,
+ uint256,
+ address,
+ address payable,
+ bytes memory,
+ address sender
+ ) public override {
+ require(to != address(0), "Cannot send to zero address");
+ require(value != 1337, "Cannot send 1337");
+ require(bytes3(data) != bytes3(0xbaddad), "Cannot call 0xbaddad");
+ // require(operation != Enum.Operation(1), "No delegate calls");
+ emit PreChecked(sender);
+ }
+
+ function checkAfterExecution(bytes32, bool) public override {
+ // revert if execution is disallowed
+ require(!executionDisallowed, "Reverted in checkAfterExecution");
+
+ emit PostChecked(true);
+ }
+
+ function setUp(bytes memory initializeParams) public {
+ address _module = abi.decode(initializeParams, (address));
+ module = _module;
+ }
+}