From a9afc867985f75113333820415f38b47f0b20c53 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:24:40 -0500 Subject: [PATCH 1/9] Add call to KeyValuePairs contract to tie HatId and streamId for tying payments to streams --- contracts/modules/DecentHatsModuleUtils.sol | 31 +++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/contracts/modules/DecentHatsModuleUtils.sol b/contracts/modules/DecentHatsModuleUtils.sol index 7436ba7..4d80b6f 100644 --- a/contracts/modules/DecentHatsModuleUtils.sol +++ b/contracts/modules/DecentHatsModuleUtils.sol @@ -9,6 +9,7 @@ import {IHats} from "../interfaces/hats/IHats.sol"; import {LockupLinear, Broker} from "../interfaces/sablier/types/DataTypes.sol"; import {IHatsModuleFactory} from "../interfaces/hats/IHatsModuleFactory.sol"; import {ISablierV2LockupLinear} from "../interfaces/sablier/ISablierV2LockupLinear.sol"; +import {KeyValuePairs} from "../KeyValuePairs.sol"; abstract contract DecentHatsModuleUtils { bytes32 public constant SALT = @@ -86,7 +87,9 @@ abstract contract DecentHatsModuleUtils { // Create streams _processSablierStreams( hatParams.sablierStreamsParams, - streamRecipient + streamRecipient, + keyValuePairs, + hatId ); unchecked { @@ -205,7 +208,9 @@ abstract contract DecentHatsModuleUtils { function _processSablierStreams( SablierStreamParams[] memory streamParams, - address streamRecipient + address streamRecipient, + KeyValuePairs keyValuePairs, + uint256 hatId ) private { for (uint256 i = 0; i < streamParams.length; ) { SablierStreamParams memory sablierStreamParams = streamParams[i]; @@ -221,6 +226,9 @@ abstract contract DecentHatsModuleUtils { ), Enum.Operation.Call ); + uint128 streamId = ISablierV2LockupLinear( + sablierStreamParams.sablier + ).nextStreamId(); // Proxy the Sablier call through IAvatar IAvatar(msg.sender).execTransactionFromModule( @@ -242,6 +250,25 @@ abstract contract DecentHatsModuleUtils { Enum.Operation.Call ); + // Update KeyValuePairs with the stream ID and Hat ID + string[] memory keys = new string[](2); + string[] memory values = new string[](2); + keys[0] = "hatId"; + values[0] = Strings.toString(hatId); + keys[1] = "streamId"; + values[1] = Strings.toString(streamId); + + IAvatar(msg.sender).execTransactionFromModule( + keyValuePairs, + 0, + abi.encodeWithSignature( + "updateValues(string[],string[])", + keys, + values + ), + Enum.Operation.Call + ); + unchecked { ++i; } From 81ebfeaca45ab07cb673d8edd06418b954662130 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:27:20 -0500 Subject: [PATCH 2/9] pass keyValuePairs as Address --- .../modules/DecentHatsCreationModule.sol | 3 ++- contracts/modules/DecentHatsModuleUtils.sol | 19 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/modules/DecentHatsCreationModule.sol b/contracts/modules/DecentHatsCreationModule.sol index 5df50a4..cc638c7 100644 --- a/contracts/modules/DecentHatsCreationModule.sol +++ b/contracts/modules/DecentHatsCreationModule.sol @@ -99,7 +99,8 @@ contract DecentHatsCreationModule is DecentHatsModuleUtils { hatsElectionsEligibilityImplementation: treeParams .hatsElectionsEligibilityImplementation, adminHatId: adminHatId, - hats: treeParams.hats + hats: treeParams.hats, + keyValuePairs: treeParams.keyValuePairs }) ); } diff --git a/contracts/modules/DecentHatsModuleUtils.sol b/contracts/modules/DecentHatsModuleUtils.sol index 4d80b6f..a2764e3 100644 --- a/contracts/modules/DecentHatsModuleUtils.sol +++ b/contracts/modules/DecentHatsModuleUtils.sol @@ -9,7 +9,7 @@ import {IHats} from "../interfaces/hats/IHats.sol"; import {LockupLinear, Broker} from "../interfaces/sablier/types/DataTypes.sol"; import {IHatsModuleFactory} from "../interfaces/hats/IHatsModuleFactory.sol"; import {ISablierV2LockupLinear} from "../interfaces/sablier/ISablierV2LockupLinear.sol"; -import {KeyValuePairs} from "../KeyValuePairs.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; abstract contract DecentHatsModuleUtils { bytes32 public constant SALT = @@ -42,6 +42,7 @@ abstract contract DecentHatsModuleUtils { address hatsAccountImplementation; uint256 topHatId; address topHatAccount; + address keyValuePairs; IHatsModuleFactory hatsModuleFactory; address hatsElectionsEligibilityImplementation; uint256 adminHatId; @@ -88,7 +89,7 @@ abstract contract DecentHatsModuleUtils { _processSablierStreams( hatParams.sablierStreamsParams, streamRecipient, - keyValuePairs, + roleHatsParams.keyValuePairs, hatId ); @@ -209,7 +210,7 @@ abstract contract DecentHatsModuleUtils { function _processSablierStreams( SablierStreamParams[] memory streamParams, address streamRecipient, - KeyValuePairs keyValuePairs, + address keyValuePairs, uint256 hatId ) private { for (uint256 i = 0; i < streamParams.length; ) { @@ -226,7 +227,7 @@ abstract contract DecentHatsModuleUtils { ), Enum.Operation.Call ); - uint128 streamId = ISablierV2LockupLinear( + uint256 streamId = ISablierV2LockupLinear( sablierStreamParams.sablier ).nextStreamId(); @@ -251,12 +252,10 @@ abstract contract DecentHatsModuleUtils { ); // Update KeyValuePairs with the stream ID and Hat ID - string[] memory keys = new string[](2); - string[] memory values = new string[](2); - keys[0] = "hatId"; - values[0] = Strings.toString(hatId); - keys[1] = "streamId"; - values[1] = Strings.toString(streamId); + string[] memory keys = new string[](1); + string[] memory values = new string[](1); + keys[0] = Strings.toString(hatId); + values[0] = Strings.toString(streamId); IAvatar(msg.sender).execTransactionFromModule( keyValuePairs, From 351a717b92b8f755e46dfdbf1a97ae38cd70203d Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:37:08 -0500 Subject: [PATCH 3/9] update test with new props --- test/modules/DecentHatsModuleUtils.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/modules/DecentHatsModuleUtils.test.ts b/test/modules/DecentHatsModuleUtils.test.ts index 6fc1d97..d5212ba 100644 --- a/test/modules/DecentHatsModuleUtils.test.ts +++ b/test/modules/DecentHatsModuleUtils.test.ts @@ -19,6 +19,8 @@ import { MockERC20__factory, GnosisSafeL2, GnosisSafeL2__factory, + KeyValuePairs, + KeyValuePairs__factory, } from '../../typechain-types'; import { getGnosisSafeL2Singleton, getGnosisSafeProxyFactory } from '../GlobalSafeDeployments.test'; @@ -44,6 +46,8 @@ describe('DecentHatsModuleUtils', () => { let mockERC20: MockERC20; let gnosisSafe: GnosisSafeL2; let gnosisSafeAddress: string; + let keyValuePairs: KeyValuePairs; + let keyValuePairsAddress: string; let topHatId: bigint; let topHatAccount: string; @@ -62,7 +66,9 @@ describe('DecentHatsModuleUtils', () => { mockHatsModuleFactory = await new MockHatsModuleFactory__factory(deployer).deploy(); mockSablier = await new MockSablierV2LockupLinear__factory(deployer).deploy(); mockERC20 = await new MockERC20__factory(deployer).deploy('MockERC20', 'MCK'); - + // deploy keyValuePairs contract + keyValuePairs = await new KeyValuePairs__factory(deployer).deploy(); + keyValuePairsAddress = await keyValuePairs.getAddress(); // Deploy Safe const gnosisSafeProxyFactory = getGnosisSafeProxyFactory(); const gnosisSafeL2Singleton = getGnosisSafeL2Singleton(); @@ -190,6 +196,7 @@ describe('DecentHatsModuleUtils', () => { mockHatsElectionsEligibilityImplementationAddress, adminHatId, hats: [hatParams], + keyValuePairs: keyValuePairsAddress, }, ], ), @@ -235,6 +242,7 @@ describe('DecentHatsModuleUtils', () => { mockHatsElectionsEligibilityImplementationAddress, adminHatId, hats: [hatParams], + keyValuePairs: keyValuePairsAddress, }, ], ), @@ -291,6 +299,7 @@ describe('DecentHatsModuleUtils', () => { mockHatsElectionsEligibilityImplementationAddress, adminHatId, hats: [hatParams], + keyValuePairs: keyValuePairsAddress, }, ], ), From 44367c3b797ad133dcb0bab05cc5112903851606 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:37:49 -0500 Subject: [PATCH 4/9] add test for new hatId/StreamId keyValuePairs event --- test/modules/DecentHatsModuleUtils.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/modules/DecentHatsModuleUtils.test.ts b/test/modules/DecentHatsModuleUtils.test.ts index d5212ba..c637bef 100644 --- a/test/modules/DecentHatsModuleUtils.test.ts +++ b/test/modules/DecentHatsModuleUtils.test.ts @@ -282,7 +282,7 @@ describe('DecentHatsModuleUtils', () => { isMutable: false, }; - await executeSafeTransaction({ + const processRoleHatTx = await executeSafeTransaction({ safe: gnosisSafe, to: await mockDecentHatsModuleUtils.getAddress(), transactionData: MockDecentHatsModuleUtils__factory.createInterface().encodeFunctionData( @@ -315,9 +315,16 @@ describe('DecentHatsModuleUtils', () => { expect(stream1.startTime).to.equal(currentBlockTimestamp); expect(stream1.endTime).to.equal(currentBlockTimestamp + 2592000); + // get the last hat created event + const hatCreatedEvents = await mockHats.queryFilter(mockHats.filters.HatCreated()); + const hatId = hatCreatedEvents[hatCreatedEvents.length - 1].args.id; const event = streamCreatedEvents[0]; + expect(event.args.sender).to.equal(await mockDecentHatsModuleUtils.getAddress()); expect(event.args.totalAmount).to.equal(hre.ethers.parseEther('100')); + await expect(processRoleHatTx) + .to.emit(keyValuePairs, 'ValueUpdated') + .withArgs(gnosisSafeAddress, hatId, streamCreatedEvents[0].args.streamId); }); }); From 78cd44c01c06c25d439c114c9e56525031fdb351 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:40:37 -0500 Subject: [PATCH 5/9] add test for termed role --- test/modules/DecentHatsModuleUtils.test.ts | 79 ++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/test/modules/DecentHatsModuleUtils.test.ts b/test/modules/DecentHatsModuleUtils.test.ts index c637bef..4e4d930 100644 --- a/test/modules/DecentHatsModuleUtils.test.ts +++ b/test/modules/DecentHatsModuleUtils.test.ts @@ -326,6 +326,85 @@ describe('DecentHatsModuleUtils', () => { .to.emit(keyValuePairs, 'ValueUpdated') .withArgs(gnosisSafeAddress, hatId, streamCreatedEvents[0].args.streamId); }); + + it('Creates a termed hat with a stream', async () => { + const currentBlockTimestamp = (await hre.ethers.provider.getBlock('latest'))!.timestamp; + const termEndDateTs = BigInt(Math.floor(Date.now() / 1000) + 100000); + const hatParams = { + wearer: wearer.address, + details: '', + imageURI: '', + sablierStreamsParams: [ + { + sablier: await mockSablier.getAddress(), + sender: await mockDecentHatsModuleUtils.getAddress(), + asset: await mockERC20.getAddress(), + timestamps: { + start: currentBlockTimestamp, + cliff: 0, + end: currentBlockTimestamp + 2592000, // 30 days + }, + broker: { account: hre.ethers.ZeroAddress, fee: 0 }, + totalAmount: hre.ethers.parseEther('100'), + cancelable: true, + transferable: false, + }, + ], + termEndDateTs, + maxSupply: 1, + isMutable: false, + }; + + const roleHatId = await mockHats.getNextId(adminHatId); + const processRoleHatTx = await executeSafeTransaction({ + safe: gnosisSafe, + to: await mockDecentHatsModuleUtils.getAddress(), + transactionData: MockDecentHatsModuleUtils__factory.createInterface().encodeFunctionData( + 'processRoleHats', + [ + { + hatsProtocol: await mockHats.getAddress(), + erc6551Registry: await erc6551Registry.getAddress(), + hatsAccountImplementation: await mockHatsAccount.getAddress(), + topHatId, + topHatAccount, + hatsModuleFactory: await mockHatsModuleFactory.getAddress(), + hatsElectionsEligibilityImplementation: + mockHatsElectionsEligibilityImplementationAddress, + adminHatId, + hats: [hatParams], + keyValuePairs: keyValuePairsAddress, + }, + ], + ), + signers: [safeSigner], + }); + + expect(await mockHats.isWearerOfHat.staticCall(wearer.address, roleHatId)).to.equal(true); + expect(await mockHats.getHatEligibilityModule(roleHatId)).to.not.equal( + hre.ethers.ZeroAddress, + ); + + const streamCreatedEvents = await mockSablier.queryFilter( + mockSablier.filters.StreamCreated(), + ); + expect(streamCreatedEvents.length).to.equal(1); + + const stream1 = await mockSablier.getStream(streamCreatedEvents[0].args.streamId); + expect(stream1.startTime).to.equal(currentBlockTimestamp); + expect(stream1.endTime).to.equal(currentBlockTimestamp + 2592000); + + // get the last hat created event + const hatCreatedEvents = await mockHats.queryFilter(mockHats.filters.HatCreated()); + const hatId = hatCreatedEvents[hatCreatedEvents.length - 1].args.id; + const event = streamCreatedEvents[0]; + + expect(event.args.sender).to.equal(await mockDecentHatsModuleUtils.getAddress()); + expect(event.args.totalAmount).to.equal(hre.ethers.parseEther('100')); + await expect(processRoleHatTx) + .to.emit(keyValuePairs, 'ValueUpdated') + .withArgs(gnosisSafeAddress, hatId, streamCreatedEvents[0].args.streamId); + }); }); describe('SALT', () => { From 7b3adbf470f8b8c983ead6607efbcd309b56a1bd Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:43:11 -0500 Subject: [PATCH 6/9] add param to other test --- test/modules/DecentHatsModificationModule.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/modules/DecentHatsModificationModule.test.ts b/test/modules/DecentHatsModificationModule.test.ts index 7ad920e..54118e3 100644 --- a/test/modules/DecentHatsModificationModule.test.ts +++ b/test/modules/DecentHatsModificationModule.test.ts @@ -282,6 +282,7 @@ describe('DecentHatsModificationModule', () => { hatsElectionsEligibilityImplementation: mockHatsElectionsEligibilityImplementationAddress, hatsModuleFactory: mockHatsModuleFactoryAddress, + keyValuePairs: await keyValuePairs.getAddress(), }, ], ), @@ -383,6 +384,7 @@ describe('DecentHatsModificationModule', () => { hatsElectionsEligibilityImplementation: mockHatsElectionsEligibilityImplementationAddress, hatsModuleFactory: mockHatsModuleFactoryAddress, + keyValuePairs: await keyValuePairs.getAddress(), }, ], ), From 753325e705425a96607cffa54fb6191fa1b890c6 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:30:13 -0500 Subject: [PATCH 7/9] pass object as value to make it easier to filter events --- contracts/modules/DecentHatsModuleUtils.sol | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/contracts/modules/DecentHatsModuleUtils.sol b/contracts/modules/DecentHatsModuleUtils.sol index a2764e3..cc3b550 100644 --- a/contracts/modules/DecentHatsModuleUtils.sol +++ b/contracts/modules/DecentHatsModuleUtils.sol @@ -254,8 +254,16 @@ abstract contract DecentHatsModuleUtils { // Update KeyValuePairs with the stream ID and Hat ID string[] memory keys = new string[](1); string[] memory values = new string[](1); - keys[0] = Strings.toString(hatId); - values[0] = Strings.toString(streamId); + keys[0] = "hatId"; + values[0] = string( + abi.encodePacked( + '{"', + Strings.toString(hatId), + '": "', + Strings.toString(streamId), + '"}' + ) + ); IAvatar(msg.sender).execTransactionFromModule( keyValuePairs, From 138a5135a6162d06f24f262826362fda8733c945 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:36:51 -0500 Subject: [PATCH 8/9] updates test to match --- test/modules/DecentHatsModuleUtils.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/modules/DecentHatsModuleUtils.test.ts b/test/modules/DecentHatsModuleUtils.test.ts index 4e4d930..c89855d 100644 --- a/test/modules/DecentHatsModuleUtils.test.ts +++ b/test/modules/DecentHatsModuleUtils.test.ts @@ -322,9 +322,10 @@ describe('DecentHatsModuleUtils', () => { expect(event.args.sender).to.equal(await mockDecentHatsModuleUtils.getAddress()); expect(event.args.totalAmount).to.equal(hre.ethers.parseEther('100')); + const expectedResult = `{"${hatId}": "${streamCreatedEvents[0].args.streamId}"}`; await expect(processRoleHatTx) .to.emit(keyValuePairs, 'ValueUpdated') - .withArgs(gnosisSafeAddress, hatId, streamCreatedEvents[0].args.streamId); + .withArgs(gnosisSafeAddress, 'hatId', expectedResult); }); it('Creates a termed hat with a stream', async () => { @@ -401,9 +402,10 @@ describe('DecentHatsModuleUtils', () => { expect(event.args.sender).to.equal(await mockDecentHatsModuleUtils.getAddress()); expect(event.args.totalAmount).to.equal(hre.ethers.parseEther('100')); + const expectedResult = `{"${hatId}": "${streamCreatedEvents[0].args.streamId}"}`; await expect(processRoleHatTx) .to.emit(keyValuePairs, 'ValueUpdated') - .withArgs(gnosisSafeAddress, hatId, streamCreatedEvents[0].args.streamId); + .withArgs(gnosisSafeAddress, 'hatId', expectedResult); }); }); From 3c567508a85778cacd88403741361cda2680a091 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:12:50 -0500 Subject: [PATCH 9/9] update key-value pair naming and adjust related tests for hatIdToStreamId --- contracts/modules/DecentHatsModuleUtils.sol | 8 +++----- test/modules/DecentHatsModuleUtils.test.ts | 8 ++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/contracts/modules/DecentHatsModuleUtils.sol b/contracts/modules/DecentHatsModuleUtils.sol index cc3b550..cd35665 100644 --- a/contracts/modules/DecentHatsModuleUtils.sol +++ b/contracts/modules/DecentHatsModuleUtils.sol @@ -254,14 +254,12 @@ abstract contract DecentHatsModuleUtils { // Update KeyValuePairs with the stream ID and Hat ID string[] memory keys = new string[](1); string[] memory values = new string[](1); - keys[0] = "hatId"; + keys[0] = "hatIdToStreamId"; values[0] = string( abi.encodePacked( - '{"', Strings.toString(hatId), - '": "', - Strings.toString(streamId), - '"}' + ":", + Strings.toString(streamId) ) ); diff --git a/test/modules/DecentHatsModuleUtils.test.ts b/test/modules/DecentHatsModuleUtils.test.ts index c89855d..09053d0 100644 --- a/test/modules/DecentHatsModuleUtils.test.ts +++ b/test/modules/DecentHatsModuleUtils.test.ts @@ -322,10 +322,10 @@ describe('DecentHatsModuleUtils', () => { expect(event.args.sender).to.equal(await mockDecentHatsModuleUtils.getAddress()); expect(event.args.totalAmount).to.equal(hre.ethers.parseEther('100')); - const expectedResult = `{"${hatId}": "${streamCreatedEvents[0].args.streamId}"}`; + const expectedResult = `${hatId}:${streamCreatedEvents[0].args.streamId}`; await expect(processRoleHatTx) .to.emit(keyValuePairs, 'ValueUpdated') - .withArgs(gnosisSafeAddress, 'hatId', expectedResult); + .withArgs(gnosisSafeAddress, 'hatIdToStreamId', expectedResult); }); it('Creates a termed hat with a stream', async () => { @@ -402,10 +402,10 @@ describe('DecentHatsModuleUtils', () => { expect(event.args.sender).to.equal(await mockDecentHatsModuleUtils.getAddress()); expect(event.args.totalAmount).to.equal(hre.ethers.parseEther('100')); - const expectedResult = `{"${hatId}": "${streamCreatedEvents[0].args.streamId}"}`; + const expectedResult = `${hatId}:${streamCreatedEvents[0].args.streamId}`; await expect(processRoleHatTx) .to.emit(keyValuePairs, 'ValueUpdated') - .withArgs(gnosisSafeAddress, 'hatId', expectedResult); + .withArgs(gnosisSafeAddress, 'hatIdToStreamId', expectedResult); }); });