Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Connection between Stream and Hat #128

Merged
merged 9 commits into from
Nov 22, 2024
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
30 changes: 28 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,23 @@ 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] = Strings.toString(hatId);
values[0] = 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
99 changes: 97 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,95 @@ 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);
});

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);
});
});

Expand Down