Skip to content

Commit

Permalink
Merge pull request #128 from decentdao/issue-interface/2479-tie-strea…
Browse files Browse the repository at this point in the history
…ms-to-hat

Create Connection between Stream and Hat
  • Loading branch information
adamgall authored Nov 22, 2024
2 parents 50072bf + 3c56750 commit beab6a6
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 5 deletions.
3 changes: 2 additions & 1 deletion contracts/modules/DecentHatsCreationModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ contract DecentHatsCreationModule is DecentHatsModuleUtils {
hatsElectionsEligibilityImplementation: treeParams
.hatsElectionsEligibilityImplementation,
adminHatId: adminHatId,
hats: treeParams.hats
hats: treeParams.hats,
keyValuePairs: treeParams.keyValuePairs
})
);
}
Expand Down
36 changes: 34 additions & 2 deletions contracts/modules/DecentHatsModuleUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {Strings} from "@openzeppelin/contracts/utils/Strings.sol";

abstract contract DecentHatsModuleUtils {
bytes32 public constant SALT =
Expand Down Expand Up @@ -41,6 +42,7 @@ abstract contract DecentHatsModuleUtils {
address hatsAccountImplementation;
uint256 topHatId;
address topHatAccount;
address keyValuePairs;
IHatsModuleFactory hatsModuleFactory;
address hatsElectionsEligibilityImplementation;
uint256 adminHatId;
Expand Down Expand Up @@ -86,7 +88,9 @@ abstract contract DecentHatsModuleUtils {
// Create streams
_processSablierStreams(
hatParams.sablierStreamsParams,
streamRecipient
streamRecipient,
roleHatsParams.keyValuePairs,
hatId
);

unchecked {
Expand Down Expand Up @@ -205,7 +209,9 @@ abstract contract DecentHatsModuleUtils {

function _processSablierStreams(
SablierStreamParams[] memory streamParams,
address streamRecipient
address streamRecipient,
address keyValuePairs,
uint256 hatId
) private {
for (uint256 i = 0; i < streamParams.length; ) {
SablierStreamParams memory sablierStreamParams = streamParams[i];
Expand All @@ -221,6 +227,9 @@ abstract contract DecentHatsModuleUtils {
),
Enum.Operation.Call
);
uint256 streamId = ISablierV2LockupLinear(
sablierStreamParams.sablier
).nextStreamId();

// Proxy the Sablier call through IAvatar
IAvatar(msg.sender).execTransactionFromModule(
Expand All @@ -242,6 +251,29 @@ abstract contract DecentHatsModuleUtils {
Enum.Operation.Call
);

// Update KeyValuePairs with the stream ID and Hat ID
string[] memory keys = new string[](1);
string[] memory values = new string[](1);
keys[0] = "hatIdToStreamId";
values[0] = string(
abi.encodePacked(
Strings.toString(hatId),
":",
Strings.toString(streamId)
)
);

IAvatar(msg.sender).execTransactionFromModule(
keyValuePairs,
0,
abi.encodeWithSignature(
"updateValues(string[],string[])",
keys,
values
),
Enum.Operation.Call
);

unchecked {
++i;
}
Expand Down
2 changes: 2 additions & 0 deletions test/modules/DecentHatsModificationModule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ describe('DecentHatsModificationModule', () => {
hatsElectionsEligibilityImplementation:
mockHatsElectionsEligibilityImplementationAddress,
hatsModuleFactory: mockHatsModuleFactoryAddress,
keyValuePairs: await keyValuePairs.getAddress(),
},
],
),
Expand Down Expand Up @@ -383,6 +384,7 @@ describe('DecentHatsModificationModule', () => {
hatsElectionsEligibilityImplementation:
mockHatsElectionsEligibilityImplementationAddress,
hatsModuleFactory: mockHatsModuleFactoryAddress,
keyValuePairs: await keyValuePairs.getAddress(),
},
],
),
Expand Down
101 changes: 99 additions & 2 deletions test/modules/DecentHatsModuleUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
MockERC20__factory,
GnosisSafeL2,
GnosisSafeL2__factory,
KeyValuePairs,
KeyValuePairs__factory,
} from '../../typechain-types';

import { getGnosisSafeL2Singleton, getGnosisSafeProxyFactory } from '../GlobalSafeDeployments.test';
Expand All @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -190,6 +196,7 @@ describe('DecentHatsModuleUtils', () => {
mockHatsElectionsEligibilityImplementationAddress,
adminHatId,
hats: [hatParams],
keyValuePairs: keyValuePairsAddress,
},
],
),
Expand Down Expand Up @@ -235,6 +242,7 @@ describe('DecentHatsModuleUtils', () => {
mockHatsElectionsEligibilityImplementationAddress,
adminHatId,
hats: [hatParams],
keyValuePairs: keyValuePairsAddress,
},
],
),
Expand Down Expand Up @@ -274,7 +282,7 @@ describe('DecentHatsModuleUtils', () => {
isMutable: false,
};

await executeSafeTransaction({
const processRoleHatTx = await executeSafeTransaction({
safe: gnosisSafe,
to: await mockDecentHatsModuleUtils.getAddress(),
transactionData: MockDecentHatsModuleUtils__factory.createInterface().encodeFunctionData(
Expand All @@ -291,6 +299,7 @@ describe('DecentHatsModuleUtils', () => {
mockHatsElectionsEligibilityImplementationAddress,
adminHatId,
hats: [hatParams],
keyValuePairs: keyValuePairsAddress,
},
],
),
Expand All @@ -306,9 +315,97 @@ 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'));
const expectedResult = `${hatId}:${streamCreatedEvents[0].args.streamId}`;
await expect(processRoleHatTx)
.to.emit(keyValuePairs, 'ValueUpdated')
.withArgs(gnosisSafeAddress, 'hatIdToStreamId', expectedResult);
});

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'));
const expectedResult = `${hatId}:${streamCreatedEvents[0].args.streamId}`;
await expect(processRoleHatTx)
.to.emit(keyValuePairs, 'ValueUpdated')
.withArgs(gnosisSafeAddress, 'hatIdToStreamId', expectedResult);
});
});

Expand Down

0 comments on commit beab6a6

Please sign in to comment.