diff --git a/.prettierrc b/.prettierrc index 937339c8..9b687cc7 100644 --- a/.prettierrc +++ b/.prettierrc @@ -7,6 +7,7 @@ "semi": true, "arrowParens": "avoid", "singleAttributePerLine": true, + "trailingComma": "all", "overrides": [ { "files": "*.sol", diff --git a/contracts/interfaces/hats/IHats.sol b/contracts/interfaces/hats/IHats.sol index c460c46d..e78f6c61 100644 --- a/contracts/interfaces/hats/IHats.sol +++ b/contracts/interfaces/hats/IHats.sol @@ -39,4 +39,9 @@ interface IHats { ) external returns (bool success); function transferHat(uint256 _hatId, address _from, address _to) external; + + function isWearerOfHat( + address _user, + uint256 _hatId + ) external view returns (bool isWearer); } diff --git a/contracts/mocks/MockHats.sol b/contracts/mocks/MockHats.sol index f5ac2ece..0eb011a0 100644 --- a/contracts/mocks/MockHats.sol +++ b/contracts/mocks/MockHats.sol @@ -5,14 +5,16 @@ import {IHats} from "../interfaces/hats/IHats.sol"; contract MockHats is IHats { uint256 public count = 0; + mapping(uint256 => address) hatWearers; function mintTopHat( - address, + address _target, string memory, string memory ) external returns (uint256 topHatId) { topHatId = count; count++; + hatWearers[topHatId] = _target; } function createHat( @@ -28,9 +30,26 @@ contract MockHats is IHats { count++; } - function mintHat(uint256, address) external pure returns (bool success) { + function mintHat( + uint256 hatId, + address wearer + ) external returns (bool success) { success = true; + hatWearers[hatId] = wearer; + } + + function transferHat(uint256 _hatId, address _from, address _to) external { + require( + hatWearers[_hatId] == _from, + "MockHats: Invalid current wearer" + ); + hatWearers[_hatId] = _to; } - function transferHat(uint256, address, address) external {} + function isWearerOfHat( + address _user, + uint256 _hatId + ) external view returns (bool isWearer) { + isWearer = hatWearers[_hatId] == _user; + } } diff --git a/test/DecentHats_0_1_0.test.ts b/test/DecentHats_0_1_0.test.ts index b04bf8f6..7ffb285e 100644 --- a/test/DecentHats_0_1_0.test.ts +++ b/test/DecentHats_0_1_0.test.ts @@ -76,7 +76,7 @@ describe('DecentHats_0_1_0', () => { hre.ethers.ZeroAddress, 0, hre.ethers.ZeroAddress, - ] + ], ); const saltNum = BigInt(`0x${Buffer.from(hre.ethers.randomBytes(32)).toString('hex')}`); @@ -85,14 +85,14 @@ describe('DecentHats_0_1_0', () => { createGnosisSetupCalldata, saltNum, gnosisSafeL2SingletonAddress, - gnosisSafeProxyFactory + gnosisSafeProxyFactory, ); gnosisSafeAddress = predictedGnosisSafeAddress; await gnosisSafeProxyFactory.createProxyWithNonce( gnosisSafeL2SingletonAddress, createGnosisSetupCalldata, - saltNum + saltNum, ); gnosisSafe = GnosisSafeL2__factory.connect(predictedGnosisSafeAddress, deployer); @@ -116,7 +116,7 @@ describe('DecentHats_0_1_0', () => { to: gnosisSafeAddress, transactionData: GnosisSafeL2__factory.createInterface().encodeFunctionData( 'enableModule', - [decentHatsAddress] + [decentHatsAddress], ), signers: [dao], }); @@ -178,7 +178,7 @@ describe('DecentHats_0_1_0', () => { }, ], }, - ] + ], ), signers: [dao], }); @@ -227,7 +227,7 @@ describe('DecentHats_0_1_0', () => { }, hats: [], }, - ] + ], ), signers: [dao], }); @@ -259,7 +259,7 @@ describe('DecentHats_0_1_0', () => { i, erc6551Registry, mockHatsAccountImplementationAddress, - mockHatsAddress + mockHatsAddress, ); expect(await topHatAccount.tokenId()).eq(i); @@ -331,7 +331,7 @@ describe('DecentHats_0_1_0', () => { }, ], }, - ] + ], ), signers: [dao], }); @@ -355,7 +355,7 @@ describe('DecentHats_0_1_0', () => { it('Creates a Sablier stream for the hat with stream parameters', async () => { const streamCreatedEvents = await mockSablier.queryFilter( - mockSablier.filters.StreamCreated() + mockSablier.filters.StreamCreated(), ); expect(streamCreatedEvents.length).to.equal(1); @@ -367,14 +367,14 @@ describe('DecentHats_0_1_0', () => { it('Does not create a Sablier stream for hats without stream parameters', async () => { const streamCreatedEvents = await mockSablier.queryFilter( - mockSablier.filters.StreamCreated() + mockSablier.filters.StreamCreated(), ); expect(streamCreatedEvents.length).to.equal(1); // Only one stream should be created }); it('Creates a Sablier stream with correct timestamps', async () => { const streamCreatedEvents = await mockSablier.queryFilter( - mockSablier.filters.StreamCreated() + mockSablier.filters.StreamCreated(), ); expect(streamCreatedEvents.length).to.equal(1); @@ -453,7 +453,7 @@ describe('DecentHats_0_1_0', () => { }, ], }, - ] + ], ), signers: [dao], }); @@ -461,7 +461,7 @@ describe('DecentHats_0_1_0', () => { it('Creates multiple Sablier streams for a single hat', async () => { const streamCreatedEvents = await mockSablier.queryFilter( - mockSablier.filters.StreamCreated() + mockSablier.filters.StreamCreated(), ); expect(streamCreatedEvents.length).to.equal(2); @@ -478,7 +478,7 @@ describe('DecentHats_0_1_0', () => { it('Creates streams with correct parameters', async () => { const streamCreatedEvents = await mockSablier.queryFilter( - mockSablier.filters.StreamCreated() + mockSablier.filters.StreamCreated(), ); const stream1 = await mockSablier.getStream(streamCreatedEvents[0].args.streamId); @@ -494,7 +494,7 @@ describe('DecentHats_0_1_0', () => { it('Creates streams with correct timestamps', async () => { const streamCreatedEvents = await mockSablier.queryFilter( - mockSablier.filters.StreamCreated() + mockSablier.filters.StreamCreated(), ); const stream1 = await mockSablier.getStream(streamCreatedEvents[0].args.streamId); @@ -508,7 +508,8 @@ describe('DecentHats_0_1_0', () => { }); describe('Creating a new hat on existing Tree', () => { - let createHatAndAccountAndMintAndStreamsTx: ethers.ContractTransactionResponse; + let createRoleHatPromise: Promise; + const topHatId = 0; beforeEach(async () => { await executeSafeTransaction({ @@ -532,25 +533,16 @@ describe('DecentHats_0_1_0', () => { wearer: ethers.ZeroAddress, sablierParams: [], }, - hats: [ - { - maxSupply: 1, - details: '', - imageURI: '', - isMutable: false, - wearer: ethers.ZeroAddress, - sablierParams: [], - }, - ], + hats: [], }, - ] + ], ), signers: [dao], }); const currentBlockTimestamp = (await hre.ethers.provider.getBlock('latest'))!.timestamp; - createHatAndAccountAndMintAndStreamsTx = await executeSafeTransaction({ + createRoleHatPromise = executeSafeTransaction({ safe: gnosisSafe, to: decentHatsAddress, transactionData: DecentHats_0_1_0__factory.createInterface().encodeFunctionData( @@ -586,24 +578,55 @@ describe('DecentHats_0_1_0', () => { await erc6551Registry.getAddress(), mockHatsAccountImplementationAddress, '0x5d0e6ce4fd951366cc55da93f6e79d8b81483109d79676a04bcc2bed6a4b5072', - ] + ], ), 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 () => { - await expect(createHatAndAccountAndMintAndStreamsTx).to.emit( - gnosisSafe, - 'ExecutionSuccess' - ); + // 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 () => { - await expect(createHatAndAccountAndMintAndStreamsTx) + // 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 + }); }); }); });