Skip to content
This repository has been archived by the owner on Oct 27, 2023. It is now read-only.

Commit

Permalink
Merge pull request 5afe#9 from 5afe/feature/plugin_meta_data
Browse files Browse the repository at this point in the history
Add logic to set and retrieve plugin meta data
  • Loading branch information
rmeissner authored Jul 13, 2023
2 parents 96d2055 + 3a94561 commit 04c36a0
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 45 deletions.
2 changes: 1 addition & 1 deletion contracts/contracts/Imports.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
pragma solidity ^0.8.18;

// Import the contract so hardhat compiles it, and we have the ABI available
import {MockContract} from "@gnosis.pm/mock-contract/contracts/MockContract.sol";
import {MockContract} from "@safe-global/mock-contract/contracts/MockContract.sol";
89 changes: 89 additions & 0 deletions contracts/contracts/Plugins.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.18;

import {ISafe} from "./interfaces/Accounts.sol";
import {ISafeProtocolPlugin} from "./interfaces/Integrations.sol";
import {ISafeProtocolManager} from "./interfaces/Manager.sol";
import {SafeTransaction, SafeRootAccess} from "./DataTypes.sol";

enum MetaDataProviderType {
IPFS,
URL,
Contract,
Event
}

interface MetaDataProvider {
function retrieveMetaData(bytes32 metaDataHash) external view returns (bytes memory metaData);
}

struct PluginMetaData {
string name;
string version;
bool requiresRootAccess;
string iconUrl;
string appUrl;
}

library PluginMetaDataOps {
function encode(PluginMetaData memory data) internal pure returns (bytes memory) {
return
abi.encodePacked(
uint8(0x00), // Format
uint8(0x00), // Format version
abi.encode(data.name, data.version, data.requiresRootAccess, data.iconUrl, data.appUrl) // Meta Data
);
}

function decode(bytes calldata data) internal pure returns (PluginMetaData memory) {
require(bytes16(data[0:2]) == bytes16(0x0000), "Unsupported format or format version");
(string memory name, string memory version, bool requiresRootAccess, string memory iconUrl, string memory appUrl) = abi.decode(
data[2:],
(string, string, bool, string, string)
);
return PluginMetaData(name, version, requiresRootAccess, iconUrl, appUrl);
}
}

abstract contract BasePlugin is ISafeProtocolPlugin, MetaDataProvider {
using PluginMetaDataOps for PluginMetaData;

string public name;
string public version;
bool public immutable requiresRootAccess;
bytes32 public immutable metaDataHash;
bytes private encodedMetaData;

constructor(PluginMetaData memory metaData) {
name = metaData.name;
version = metaData.version;
requiresRootAccess = metaData.requiresRootAccess;
// MetaData Format + Format Version + Encoded MetaData
encodedMetaData = metaData.encode();
metaDataHash = keccak256(encodedMetaData);
}

function metaProvider() external view override returns (uint256 providerType, bytes memory location) {
providerType = uint256(MetaDataProviderType.Contract);
location = abi.encode(address(this));
}

function retrieveMetaData(bytes32 _metaDataHash) external view returns (bytes memory metaData) {
require(metaDataHash == _metaDataHash, "Cannot retrieve meta data");
return encodedMetaData;
}
}

contract SamplePlugin is BasePlugin {
constructor()
BasePlugin(PluginMetaData({name: "Sample Plugin", version: "1.0.0", requiresRootAccess: false, iconUrl: "", appUrl: ""}))
{}

function executeFromPlugin(
ISafeProtocolManager manager,
ISafe safe,
SafeTransaction calldata safetx
) external returns (bytes[] memory data) {
(data) = manager.executeTransaction(safe, safetx);
}
}
33 changes: 0 additions & 33 deletions contracts/contracts/SamplePlugin.sol

This file was deleted.

4 changes: 2 additions & 2 deletions contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
"prepack": "yarn build"
},
"devDependencies": {
"@gnosis.pm/mock-contract": "gnosis/mock-contract#b0f735ddc62d5000b50667011d69142a4dee9c71",
"@gnosis.pm/safe-singleton-factory": "^1.0.14",
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
"@nomicfoundation/hardhat-ethers": "^3.0.0",
"@nomicfoundation/hardhat-network-helpers": "^1.0.0",
"@nomicfoundation/hardhat-toolbox": "^3.0.0",
"@nomicfoundation/hardhat-verify": "^1.0.0",
"@openzeppelin/contracts": "^4.9.1",
"@safe-global/mock-contract": "^4.0.0",
"@gnosis.pm/safe-singleton-factory": "^1.0.14",
"@typechain/ethers-v6": "^0.4.0",
"@typechain/hardhat": "^8.0.0",
"@types/chai": "^4.2.0",
Expand Down
12 changes: 12 additions & 0 deletions contracts/test/SamplePlugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import hre, { deployments } from "hardhat";
import { expect } from "chai";
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
import { getSamplePlugin } from "./utils/contracts";
import { loadPluginMetaData } from "./utils/metadata";

describe("SamplePlugin", async () => {
let user1: SignerWithAddress;
Expand All @@ -24,4 +25,15 @@ describe("SamplePlugin", async () => {
expect(await plugin.version()).to.be.eq("1.0.0");
expect(await plugin.requiresRootAccess()).to.be.false;
});

it("can retrieve meta data for module", async () => {
const { plugin } = await setup()
expect(await loadPluginMetaData(plugin)).to.be.deep.eq({
name: 'Sample Plugin',
version: '1.0.0',
requiresRootAccess: false,
iconUrl: '',
appUrl: ''
});
});
});
14 changes: 9 additions & 5 deletions contracts/test/utils/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { BaseContract } from "ethers";
import { Addressable, BaseContract } from "ethers";
import hre, { deployments } from "hardhat";
import { SamplePlugin } from "../../typechain-types";

export const getInstance = async<T extends BaseContract>(name: string): Promise<T> => {
export const getInstance = async<T extends BaseContract>(name: string, address: string): Promise<T> => {
// TODO: this typecasting should be refactored
return (await hre.ethers.getContractAt(name, address) as unknown) as T;
};

export const getSingleton = async<T extends BaseContract>(name: string): Promise<T> => {
const deployment = await deployments.get(name);
const Contract = await hre.ethers.getContractFactory(name);
return Contract.attach(deployment.address) as T;
return getInstance<T>(name, deployment.address)
};

export const getSamplePlugin = () => getInstance<SamplePlugin>("SamplePlugin")
export const getSamplePlugin = () => getSingleton<SamplePlugin>("SamplePlugin")
64 changes: 64 additions & 0 deletions contracts/test/utils/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { AbiCoder, ParamType, isHexString, keccak256 } from "ethers"
import { BasePlugin, MetaDataProvider, MetaDataProvider__factory } from "../../typechain-types"
import { getInstance } from "./contracts";

interface PluginMetaData {
name: string,
version: string,
requiresRootAccess: boolean,
iconUrl: string,
appUrl: string
}

const ProviderType_IPFS = 0n;
const ProviderType_URL = 1n;
const ProviderType_Contract = 2n;
const ProviderType_Event = 3n;

const PluginMetaDataType: string[] = [
"string name",
"string version",
"bool requiresRootAccess",
"string iconUrl",
"string appUrl"
]

const loadPluginMetaDataFromContract = async (provider: string, metaDataHash: string): Promise<string> => {
const providerInstance = await getInstance<MetaDataProvider>("MetaDataProvider", provider)
return await providerInstance.retrieveMetaData(metaDataHash)
}

const loadRawMetaData = async(plugin: BasePlugin, metaDataHash: string): Promise<string> => {
const [type, source] = await plugin.metaProvider()
switch(type) {
case ProviderType_Contract:
return loadPluginMetaDataFromContract(AbiCoder.defaultAbiCoder().decode(["address"], source)[0], metaDataHash)
default:
throw Error("Unsupported MetaDataProviderType")
}
}

export const loadPluginMetaData = async (plugin: BasePlugin): Promise<PluginMetaData> => {
const metaDataHash = await plugin.metaDataHash()
const metaData = await loadRawMetaData(plugin, metaDataHash)
if (metaDataHash !== keccak256(metaData)) throw Error("Invalid meta data retrieved!")
return decodePluginMetaData(metaData)
}

export const decodePluginMetaData = (data: string): PluginMetaData => {
if (!isHexString(data)) throw Error("Invalid data format");
const format = data.slice(2, 6)
if (format !== "0000") throw Error("Unsupported format or format version");
const metaData = data.slice(6)
const decoded = AbiCoder.defaultAbiCoder().decode(
PluginMetaDataType,
"0x" + metaData
)
return {
name: decoded[0],
version: decoded[1],
requiresRootAccess: decoded[2],
iconUrl: decoded[3],
appUrl: decoded[4]
}
}
9 changes: 5 additions & 4 deletions contracts/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -450,10 +450,6 @@
"@ethersproject/properties" "^5.7.0"
"@ethersproject/strings" "^5.7.0"

"@gnosis.pm/mock-contract@gnosis/mock-contract#b0f735ddc62d5000b50667011d69142a4dee9c71":
version "4.0.0"
resolved "https://codeload.github.com/gnosis/mock-contract/tar.gz/b0f735ddc62d5000b50667011d69142a4dee9c71"

"@gnosis.pm/safe-singleton-factory@^1.0.14":
version "1.0.14"
resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-singleton-factory/-/safe-singleton-factory-1.0.14.tgz#42dae9a91fda21b605f94bfe310a7fccc6a4d738"
Expand Down Expand Up @@ -792,6 +788,11 @@
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.1.tgz#afa804d2c68398704b0175acc94d91a54f203645"
integrity sha512-aLDTLu/If1qYIFW5g4ZibuQaUsFGWQPBq1mZKp/txaebUnGHDmmiBhRLY1tDNedN0m+fJtKZ1zAODS9Yk+V6uA==

"@safe-global/mock-contract@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@safe-global/mock-contract/-/mock-contract-4.0.0.tgz#8e1e17e93af5d4b343a6bb6cef8c1f513cb7a92e"
integrity sha512-6ijStTgQI6JzYe8Nsc4j1VW4XQ89qCl7ZkRGxwwlxnaOMDYzVekwPACbg2kDDzhtJ4p8vSvE6ZroxSkvP7610A==

"@scure/base@~1.1.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
Expand Down

0 comments on commit 04c36a0

Please sign in to comment.