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 top hat #91

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions contracts/DecentHats.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//SPDX-License-Identifier: MIT
pragma solidity =0.8.19;

import { Enum } from "@gnosis.pm/zodiac/contracts/core/Module.sol";
import { IAvatar } from "@gnosis.pm/zodiac/contracts/interfaces/IAvatar.sol";
import { IHats } from "./interfaces/hats/IHats.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";

contract DecentHats {
IHats public hats;
address public keyValuePairs;

constructor(IHats _hats, address _keyValuePairs) {
hats = _hats;
keyValuePairs = _keyValuePairs;
}

function createAndDeclareTree(string memory _details, string memory _imageURI) public returns (bool success) {
uint256 topHatId = hats.mintTopHat(msg.sender, _details, _imageURI);

string[] memory keys = new string[](1);
keys[0] = "hatsTreeId";

string[] memory values = new string[](1);
values[0] = Strings.toString(topHatId);

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

return success;
}
}
23 changes: 23 additions & 0 deletions contracts/interfaces/hats/IHats.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: AGPL-3.0
// Copyright (C) 2023 Haberdasher Labs
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

pragma solidity >=0.8.13;

interface IHats {
function mintTopHat(address _target, string memory _details, string memory _imageURI)
external
returns (uint256 topHatId);
}
13 changes: 13 additions & 0 deletions contracts/mock/MockHats.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
pragma solidity =0.8.19;

import { IHats } from "../interfaces/hats/IHats.sol";

contract MockHats is IHats {
uint256 count = 0;

function mintTopHat(address, string memory, string memory) external returns (uint256 topHatId) {
topHatId = count;
count++;
}
}
225 changes: 225 additions & 0 deletions test/DecentHats.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import {
GnosisSafeL2,
GnosisSafeL2__factory,
DecentHats__factory,
KeyValuePairs,
KeyValuePairs__factory,
MockHats__factory,
} from "../typechain-types";

import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
import { expect } from "chai";
import { ethers } from "ethers";
import hre from "hardhat";

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

describe("DecentHats", () => {
let dao1: SignerWithAddress;
let dao2: SignerWithAddress;

let keyValuePairs: KeyValuePairs;
let gnosisSafe: GnosisSafeL2;

let gnosisSafeAddress: string;
let decentHatsAddress: string;

const saltNum = BigInt(
`0x${Buffer.from(ethers.randomBytes(32)).toString("hex")}`
);

beforeEach(async () => {
const signers = await hre.ethers.getSigners();
const [deployer] = signers;
[, dao1, dao2] = signers;

const hats = await new MockHats__factory(deployer).deploy();
keyValuePairs = await new KeyValuePairs__factory(deployer).deploy();
const decentHats = await new DecentHats__factory(deployer).deploy(
await hats.getAddress(),
await keyValuePairs.getAddress()
);
decentHatsAddress = await decentHats.getAddress();

const gnosisSafeProxyFactory = getGnosisSafeProxyFactory();
const gnosisSafeL2Singleton = getGnosisSafeL2Singleton();
const gnosisSafeL2SingletonAddress =
await gnosisSafeL2Singleton.getAddress();

const createGnosisSetupCalldata =
GnosisSafeL2__factory.createInterface().encodeFunctionData("setup", [
[dao1.address],
1,
ethers.ZeroAddress,
ethers.ZeroHash,
ethers.ZeroAddress,
ethers.ZeroAddress,
0,
ethers.ZeroAddress,
]);

const predictedGnosisSafeAddress = await predictGnosisSafeAddress(
createGnosisSetupCalldata,
saltNum,
gnosisSafeL2SingletonAddress,
gnosisSafeProxyFactory
);
gnosisSafeAddress = predictedGnosisSafeAddress;

await gnosisSafeProxyFactory.createProxyWithNonce(
gnosisSafeL2SingletonAddress,
createGnosisSetupCalldata,
saltNum
);

gnosisSafe = GnosisSafeL2__factory.connect(
predictedGnosisSafeAddress,
deployer
);
});

describe("DecentHats as a Module", () => {
let enableModuleTx: ethers.ContractTransactionResponse;

beforeEach(async () => {
enableModuleTx = await executeSafeTransaction({
safe: gnosisSafe,
to: gnosisSafeAddress,
transactionData:
GnosisSafeL2__factory.createInterface().encodeFunctionData(
"enableModule",
[decentHatsAddress]
),
signers: [dao1],
});
});

it("Emits an ExecutionSuccess event", async () => {
await expect(enableModuleTx).to.emit(gnosisSafe, "ExecutionSuccess");
});

it("Emits an EnabledModule event", async () => {
await expect(enableModuleTx)
.to.emit(gnosisSafe, "EnabledModule")
.withArgs(decentHatsAddress);
});

describe("Creating a new Top Hat and Tree", () => {
let createAndDeclareTreeTx: ethers.ContractTransactionResponse;

beforeEach(async () => {
createAndDeclareTreeTx = await executeSafeTransaction({
safe: gnosisSafe,
to: decentHatsAddress,
transactionData:
DecentHats__factory.createInterface().encodeFunctionData(
"createAndDeclareTree",
["", ""]
),
signers: [dao1],
});
});

it("Emits an ExecutionSuccess event", async () => {
await expect(createAndDeclareTreeTx).to.emit(
gnosisSafe,
"ExecutionSuccess"
);
});

it("Emits an ExecutionFromModuleSuccess event", async () => {
await expect(createAndDeclareTreeTx)
.to.emit(gnosisSafe, "ExecutionFromModuleSuccess")
.withArgs(decentHatsAddress);
});

it("Emits a hatsTreeId ValueUpdated event", async () => {
await expect(createAndDeclareTreeTx)
.to.emit(keyValuePairs, "ValueUpdated")
.withArgs(gnosisSafeAddress, "hatsTreeId", "0");
});

describe("Multiple calls", () => {
let createAndDeclareTreeTx2: ethers.ContractTransactionResponse;

beforeEach(async () => {
createAndDeclareTreeTx2 = await executeSafeTransaction({
safe: gnosisSafe,
to: decentHatsAddress,
transactionData:
DecentHats__factory.createInterface().encodeFunctionData(
"createAndDeclareTree",
["", ""]
),
signers: [dao1],
});
});

it("Emits an ExecutionSuccess event", async () => {
await expect(createAndDeclareTreeTx2).to.emit(
gnosisSafe,
"ExecutionSuccess"
);
});

it("Emits an ExecutionFromModuleSuccess event", async () => {
await expect(createAndDeclareTreeTx2)
.to.emit(gnosisSafe, "ExecutionFromModuleSuccess")
.withArgs(decentHatsAddress);
});

it("Creates Top Hats with sequential IDs", async () => {
await expect(createAndDeclareTreeTx2)
.to.emit(keyValuePairs, "ValueUpdated")
.withArgs(gnosisSafeAddress, "hatsTreeId", "1");
});
});
});
});
});