diff --git a/test/DecentHats.test.ts b/test/DecentHats.test.ts index 8063981..b0fd891 100644 --- a/test/DecentHats.test.ts +++ b/test/DecentHats.test.ts @@ -1,6 +1,9 @@ import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; import { expect } from 'chai'; -import hre, { ethers } from 'hardhat'; +/* eslint-disable-next-line import/no-extraneous-dependencies */ +import { ethers } from 'ethers'; +import hre from 'hardhat'; + import { GnosisSafeL2, GnosisSafeL2__factory, @@ -18,58 +21,16 @@ import { MockSablierV2LockupLinear, MockERC20__factory, MockERC20, - MockHatsModuleFactory__factory, + DecentAutonomousAdmin, + DecentAutonomousAdmin__factory, MockHatsElectionEligibility__factory, - ModuleProxyFactory__factory, + MockHatsModuleFactory__factory, ModuleProxyFactory, - DecentAutonomousAdmin__factory, - DecentAutonomousAdmin, + ModuleProxyFactory__factory, } from '../typechain-types'; import { getGnosisSafeL2Singleton, getGnosisSafeProxyFactory } from './GlobalSafeDeployments.test'; -import { - buildSafeTransaction, - buildSignatureBytes, - predictGnosisSafeAddress, - safeSignTypedData, -} from './helpers'; - -const executeSafeTransaction = async ({ - safe, - to, - transactionData, - signers, -}: { - safe: GnosisSafeL2; - to: string; - transactionData: string; - signers: SignerWithAddress[]; -}) => { - const safeTx = buildSafeTransaction({ - to, - data: transactionData, - nonce: await safe.nonce(), - }); - - const sigs = await Promise.all( - signers.map(async signer => safeSignTypedData(signer, safe, safeTx)), - ); - - const tx = await safe.execTransaction( - safeTx.to, - safeTx.value, - safeTx.data, - safeTx.operation, - safeTx.safeTxGas, - safeTx.baseGas, - safeTx.gasPrice, - safeTx.gasToken, - safeTx.refundReceiver, - buildSignatureBytes(sigs), - ); - - return tx; -}; +import { executeSafeTransaction, getHatAccount, predictGnosisSafeAddress } from './helpers'; describe('DecentHats', () => { let dao: SignerWithAddress; @@ -100,74 +61,75 @@ describe('DecentHats', () => { let moduleProxyFactory: ModuleProxyFactory; let decentAutonomousAdminMasterCopy: DecentAutonomousAdmin; - beforeEach(async () => { - const signers = await hre.ethers.getSigners(); - const [deployer] = signers; - [, dao] = signers; - - mockHats = await new MockHats__factory(deployer).deploy(); - mockHatsAddress = await mockHats.getAddress(); - const mockHatsElectionEligibilityImplementation = - await new MockHatsElectionEligibility__factory(deployer).deploy(); - mockHatsElectionEligibilityImplementationAddress = - await mockHatsElectionEligibilityImplementation.getAddress(); - const mockHatsModuleFactory = await new MockHatsModuleFactory__factory(deployer).deploy(); - mockHatsModuleFactoryAddress = await mockHatsModuleFactory.getAddress(); - keyValuePairs = await new KeyValuePairs__factory(deployer).deploy(); - erc6551Registry = await new ERC6551Registry__factory(deployer).deploy(); - mockHatsAccountImplementation = await new MockHatsAccount__factory(deployer).deploy(); - mockHatsAccountImplementationAddress = await mockHatsAccountImplementation.getAddress(); - decentHats = await new DecentHats__factory(deployer).deploy(); - decentHatsAddress = await decentHats.getAddress(); - - moduleProxyFactory = await new ModuleProxyFactory__factory(deployer).deploy(); - decentAutonomousAdminMasterCopy = await new DecentAutonomousAdmin__factory(deployer).deploy(); - - const gnosisSafeProxyFactory = getGnosisSafeProxyFactory(); - const gnosisSafeL2Singleton = getGnosisSafeL2Singleton(); - const gnosisSafeL2SingletonAddress = await gnosisSafeL2Singleton.getAddress(); - - const createGnosisSetupCalldata = GnosisSafeL2__factory.createInterface().encodeFunctionData( - 'setup', - [ - [dao.address], - 1, - hre.ethers.ZeroAddress, - hre.ethers.ZeroHash, - hre.ethers.ZeroAddress, - hre.ethers.ZeroAddress, - 0, - hre.ethers.ZeroAddress, - ], - ); - - const saltNum = BigInt(`0x${Buffer.from(hre.ethers.randomBytes(32)).toString('hex')}`); - - const predictedGnosisSafeAddress = await predictGnosisSafeAddress( - createGnosisSetupCalldata, - saltNum, - gnosisSafeL2SingletonAddress, - gnosisSafeProxyFactory, - ); - gnosisSafeAddress = predictedGnosisSafeAddress; - - await gnosisSafeProxyFactory.createProxyWithNonce( - gnosisSafeL2SingletonAddress, - createGnosisSetupCalldata, - saltNum, - ); - - gnosisSafe = GnosisSafeL2__factory.connect(predictedGnosisSafeAddress, deployer); - - // Deploy MockSablierV2LockupLinear - mockSablier = await new MockSablierV2LockupLinear__factory(deployer).deploy(); - mockSablierAddress = await mockSablier.getAddress(); - - mockERC20 = await new MockERC20__factory(deployer).deploy('MockERC20', 'MCK'); - mockERC20Address = await mockERC20.getAddress(); - - await mockERC20.mint(gnosisSafeAddress, ethers.parseEther('1000000')); + try { + const signers = await hre.ethers.getSigners(); + const [deployer] = signers; + [, dao] = signers; + + mockHats = await new MockHats__factory(deployer).deploy(); + mockHatsAddress = await mockHats.getAddress(); + const mockHatsElectionEligibilityImplementation = + await new MockHatsElectionEligibility__factory(deployer).deploy(); + mockHatsElectionEligibilityImplementationAddress = + await mockHatsElectionEligibilityImplementation.getAddress(); + const mockHatsModuleFactory = await new MockHatsModuleFactory__factory(deployer).deploy(); + mockHatsModuleFactoryAddress = await mockHatsModuleFactory.getAddress(); + keyValuePairs = await new KeyValuePairs__factory(deployer).deploy(); + erc6551Registry = await new ERC6551Registry__factory(deployer).deploy(); + mockHatsAccountImplementation = await new MockHatsAccount__factory(deployer).deploy(); + mockHatsAccountImplementationAddress = await mockHatsAccountImplementation.getAddress(); + decentHats = await new DecentHats__factory(deployer).deploy(); + decentHatsAddress = await decentHats.getAddress(); + moduleProxyFactory = await new ModuleProxyFactory__factory(deployer).deploy(); + decentAutonomousAdminMasterCopy = await new DecentAutonomousAdmin__factory(deployer).deploy(); + + const gnosisSafeProxyFactory = getGnosisSafeProxyFactory(); + const gnosisSafeL2Singleton = getGnosisSafeL2Singleton(); + const gnosisSafeL2SingletonAddress = await gnosisSafeL2Singleton.getAddress(); + + const createGnosisSetupCalldata = GnosisSafeL2__factory.createInterface().encodeFunctionData( + 'setup', + [ + [dao.address], + 1, + hre.ethers.ZeroAddress, + hre.ethers.ZeroHash, + hre.ethers.ZeroAddress, + hre.ethers.ZeroAddress, + 0, + hre.ethers.ZeroAddress, + ], + ); + const saltNum = BigInt(`0x${Buffer.from(hre.ethers.randomBytes(32)).toString('hex')}`); + + const predictedGnosisSafeAddress = await predictGnosisSafeAddress( + createGnosisSetupCalldata, + saltNum, + gnosisSafeL2SingletonAddress, + gnosisSafeProxyFactory, + ); + gnosisSafeAddress = predictedGnosisSafeAddress; + + await gnosisSafeProxyFactory.createProxyWithNonce( + gnosisSafeL2SingletonAddress, + createGnosisSetupCalldata, + saltNum, + ); + + gnosisSafe = GnosisSafeL2__factory.connect(predictedGnosisSafeAddress, deployer); + + // Deploy MockSablierV2LockupLinear + mockSablier = await new MockSablierV2LockupLinear__factory(deployer).deploy(); + mockSablierAddress = await mockSablier.getAddress(); + + mockERC20 = await new MockERC20__factory(deployer).deploy('MockERC20', 'MCK'); + mockERC20Address = await mockERC20.getAddress(); + + await mockERC20.mint(gnosisSafeAddress, ethers.parseEther('1000000')); + } catch (e) { + console.error('AHHHHHH', e); + } }); describe('DecentHats as a Module', () => { @@ -220,12 +182,7 @@ describe('DecentHats', () => { wearer: ethers.ZeroAddress, sablierParams: [], isTermed: false, - termedParams: [ - { - termEndDateTs: 0, - nominatedWearers: [], - }, - ], + termedParams: [], }, hats: [ { @@ -236,12 +193,7 @@ describe('DecentHats', () => { wearer: ethers.ZeroAddress, sablierParams: [], isTermed: false, - termedParams: [ - { - termEndDateTs: 0, - nominatedWearers: [], - }, - ], + termedParams: [], }, { maxSupply: 1, @@ -251,12 +203,7 @@ describe('DecentHats', () => { wearer: ethers.ZeroAddress, sablierParams: [], isTermed: false, - termedParams: [ - { - termEndDateTs: 0, - nominatedWearers: [], - }, - ], + termedParams: [], }, ], hatsModuleFactory: mockHatsModuleFactoryAddress, @@ -349,34 +296,17 @@ describe('DecentHats', () => { }); describe('Creating Hats Accounts', () => { - let salt: string; - - beforeEach(async () => { - salt = await decentHats.SALT(); - }); - - const getHatAccount = async (hatId: bigint) => { - const hatAccountAddress = await erc6551Registry.account( - mockHatsAccountImplementationAddress, - salt, - await hre.getChainId(), - mockHatsAddress, - hatId, - ); - - const hatAccount = MockHatsAccount__factory.connect( - hatAccountAddress, - hre.ethers.provider, - ); - - return hatAccount; - }; - it('Generates the correct Addresses for the current Hats', async () => { const currentCount = await mockHats.count(); for (let i = 0n; i < currentCount; i++) { - const topHatAccount = await getHatAccount(i); + const topHatAccount = await getHatAccount( + i, + erc6551Registry, + mockHatsAccountImplementationAddress, + mockHatsAddress, + ); + expect(await topHatAccount.tokenId()).eq(i); expect(await topHatAccount.tokenImplementation()).eq(mockHatsAddress); } @@ -666,5 +596,141 @@ describe('DecentHats', () => { expect(stream2.endTime).to.equal(currentBlockTimestamp + 1296000); }); }); + + describe('Creating a new hat on existing Tree', () => { + let createRoleHatPromise: Promise; + const topHatId = 0; + + beforeEach(async () => { + try { + await executeSafeTransaction({ + safe: gnosisSafe, + to: decentHatsAddress, + transactionData: DecentHats__factory.createInterface().encodeFunctionData( + 'createAndDeclareTree', + [ + { + hatsProtocol: mockHatsAddress, + hatsAccountImplementation: mockHatsAccountImplementationAddress, + registry: await erc6551Registry.getAddress(), + keyValuePairs: await keyValuePairs.getAddress(), + topHatDetails: '', + topHatImageURI: '', + adminHat: { + maxSupply: 1, + details: '', + imageURI: '', + isMutable: false, + wearer: ethers.ZeroAddress, + sablierParams: [], + isTermed: false, + termedParams: [], + }, + hatsModuleFactory: mockHatsModuleFactoryAddress, + hatsElectionEligibilityImplementation: + mockHatsElectionEligibilityImplementationAddress, + moduleProxyFactory: await moduleProxyFactory.getAddress(), + decentAutonomousAdminMasterCopy: + await decentAutonomousAdminMasterCopy.getAddress(), + hats: [], + }, + ], + ), + signers: [dao], + }); + } catch (e) { + console.error('Error creating tree', e); + } + const currentBlockTimestamp = (await hre.ethers.provider.getBlock('latest'))!.timestamp; + + createRoleHatPromise = executeSafeTransaction({ + safe: gnosisSafe, + to: decentHatsAddress, + transactionData: DecentHats__factory.createInterface().encodeFunctionData( + 'createRoleHat', + [ + { + hatsProtocol: mockHatsAddress, + registry: await erc6551Registry.getAddress(), + topHatAccount: '0xdce7ca0555101f97451926944f5ae3b7adb2f5ae', + hatsAccountImplementation: mockHatsAccountImplementationAddress, + adminHatId: 1, + topHatId, + hat: { + maxSupply: 1, + details: '', + imageURI: '', + isMutable: true, + wearer: '0xdce7ca0555101f97451926944f5ae3b7adb2f5ae', + isTermed: false, + termedParams: [], + sablierParams: [ + { + sablier: mockSablierAddress, + sender: gnosisSafeAddress, + totalAmount: ethers.parseEther('100'), + asset: mockERC20Address, + cancelable: true, + transferable: false, + timestamps: { + start: currentBlockTimestamp, + cliff: currentBlockTimestamp + 86400, // 1 day cliff + end: currentBlockTimestamp + 2592000, // 30 days from now + }, + broker: { account: ethers.ZeroAddress, fee: 0 }, + }, + ], + }, + }, + ], + ), + signers: [dao], + }); + }); + + it('Reverts if the top hat is not transferred to the DecentHats module first', async () => { + await expect(createRoleHatPromise).to.be.reverted; + }); + + it('Emits an ExecutionSuccess event', async () => { + // First transfer the top hat to the Safe + await mockHats.transferHat(topHatId, gnosisSafeAddress, decentHatsAddress); + await expect(await createRoleHatPromise).to.emit(gnosisSafe, 'ExecutionSuccess'); + }); + + it('Emits an ExecutionFromModuleSuccess event', async () => { + // First transfer the top hat to the Safe + await mockHats.transferHat(topHatId, gnosisSafeAddress, decentHatsAddress); + await expect(await createRoleHatPromise) + .to.emit(gnosisSafe, 'ExecutionFromModuleSuccess') + .withArgs(decentHatsAddress); + }); + + it('Transfers the top hat back to the Safe', async () => { + // First transfer the top hat to the Safe + await mockHats.transferHat(topHatId, gnosisSafeAddress, decentHatsAddress); + + const isModuleWearerOfTopHat = await mockHats.isWearerOfHat(decentHatsAddress, topHatId); + expect(isModuleWearerOfTopHat).to.equal(true); + + await createRoleHatPromise; + + const isSafeWearerOfTopHat = await mockHats.isWearerOfHat(gnosisSafeAddress, topHatId); + expect(isSafeWearerOfTopHat).to.equal(true); + }); + + it('Actually creates the new hat', async () => { + // First transfer the top hat to the Safe + await mockHats.transferHat(topHatId, gnosisSafeAddress, decentHatsAddress); + + const hatsCountBeforeCreate = await mockHats.count(); + expect(hatsCountBeforeCreate).to.equal(2); // Top hat + admin hat + + await createRoleHatPromise; + + const newHatId = await mockHats.count(); + expect(newHatId).to.equal(3); // + newly created hat + }); + }); }); }); diff --git a/test/DecentHats_0_1_0.test.ts b/test/DecentHats_0_1_0.test.ts index 7ffb285..dd9a365 100644 --- a/test/DecentHats_0_1_0.test.ts +++ b/test/DecentHats_0_1_0.test.ts @@ -1,6 +1,8 @@ import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; import { expect } from 'chai'; -import hre, { ethers } from 'hardhat'; +/* eslint-disable-next-line import/no-extraneous-dependencies */ +import { ethers } from 'ethers'; +import hre from 'hardhat'; import { GnosisSafeL2, GnosisSafeL2__factory, diff --git a/test/helpers.ts b/test/helpers.ts index 1782629..d8b42c2 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,5 +1,7 @@ import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; -import hre, { ethers } from 'hardhat'; +/* eslint-disable-next-line import/no-extraneous-dependencies */ +import { ethers } from 'ethers'; +import hre from 'hardhat'; import { ERC6551Registry, GnosisSafeL2,