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

metadata contracts #105

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions contracts/src/mocks/utils/metadata/MetadataExtensionMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.8;

import {MetadataExtension} from "../../../utils/metadata/MetadataExtension.sol";
import {IDAO} from "../../../dao/IDAO.sol";
import {DaoAuthorizable} from "../../../permission/auth/DaoAuthorizable.sol";

/// @notice A mock contract.
/// @dev DO NOT USE IN PRODUCTION!
contract MetadataExtensionMock is MetadataExtension {
constructor(IDAO dao) DaoAuthorizable(dao) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.8;

import {MetadataExtensionUpgradeable} from "../../../utils/metadata/MetadataExtensionUpgradeable.sol";

import {IDAO} from "../../../dao/IDAO.sol";

/// @notice A mock contract.
/// @dev DO NOT USE IN PRODUCTION!
contract MetadataExtensionUpgradeableMock is MetadataExtensionUpgradeable {
function initialize(IDAO _dao) public initializer {
__DaoAuthorizableUpgradeable_init(_dao);
}
}
50 changes: 50 additions & 0 deletions contracts/src/utils/metadata/MetadataExtension.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.8;

import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";

import {DaoAuthorizable} from "../../permission/auth/DaoAuthorizable.sol";

/// @title MetadataExtension
/// @author Aragon X - 2024
/// @custom:security-contact [email protected]
abstract contract MetadataExtension is ERC165, DaoAuthorizable {
/// @notice The ID of the permission required to call the `updateMetadata` function.
bytes32 public constant UPDATE_METADATA_PERMISSION_ID = keccak256("UPDATE_METADATA_PERMISSION");

/// @notice Emitted when metadata is updated.
event MetadataUpdated(bytes metadata);

bytes private metadata;

/// @notice Checks if this or the parent contract supports an interface by its ID.
/// @param _interfaceId The ID of the interface.
/// @return Returns `true` if the interface is supported.
function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) {
return
_interfaceId == this.updateMetadata.selector ^ this.getMetadata.selector ||
super.supportsInterface(_interfaceId);
}

/// @notice Allows to update only the metadata.
/// @param _metadata The utf8 bytes of a content addressing cid that stores plugin's information.
function updateMetadata(
bytes memory _metadata
) public virtual auth(UPDATE_METADATA_PERMISSION_ID) {
_updateMetadata(_metadata);
}

/// @notice Returns the metadata currently applied.
/// @return The The utf8 bytes of a content addressing cid.
function getMetadata() public view returns (bytes memory) {
return metadata;
}

/// @notice Internal function to update metadata.
/// @param _metadata The utf8 bytes of a content addressing cid that stores contract's information.
function _updateMetadata(bytes memory _metadata) internal virtual {
metadata = _metadata;
emit MetadataUpdated(_metadata);
}
}
71 changes: 71 additions & 0 deletions contracts/src/utils/metadata/MetadataExtensionUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.8;

import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol";

import {DaoAuthorizableUpgradeable} from "../../permission/auth/DaoAuthorizableUpgradeable.sol";

/// @title MetadataExtensionUpgradeable
/// @dev Due to the requirements that already existing upgradeable plugins need to start inheritting from this,
/// we're required to use hardcoded/specific slots for storage instead of sequential slots with gaps.
/// @author Aragon X - 2024
/// @custom:security-contact [email protected]
abstract contract MetadataExtensionUpgradeable is ERC165Upgradeable, DaoAuthorizableUpgradeable {
/// @notice The ID of the permission required to call the `updateMetadata` function.
bytes32 public constant UPDATE_METADATA_PERMISSION_ID = keccak256("UPDATE_METADATA_PERMISSION");

// keccak256(abi.encode(uint256(keccak256("osx-commons.storage.MetadataExtension")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant MetadataExtensionStorageLocation =
0x47ff9796f72d439c6e5c30a24b9fad985a00c85a9f2258074c400a94f8746b00;

/// @notice Emitted when metadata is updated.
event MetadataUpdated(bytes metadata);

struct MetadataExtensionStorage {
bytes metadata;
}

function _getMetadataExtensionStorage()
private
pure
returns (MetadataExtensionStorage storage $)
{
assembly {
$.slot := MetadataExtensionStorageLocation
}
}

/// @notice Checks if this or the parent contract supports an interface by its ID.
/// @param _interfaceId The ID of the interface.
/// @return Returns `true` if the interface is supported.
function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) {
return
_interfaceId == this.updateMetadata.selector ^ this.getMetadata.selector ||
super.supportsInterface(_interfaceId);
}

/// @notice Allows to update only the metadata.
/// @param _metadata The utf8 bytes of a content addressing cid that stores plugin's information.
function updateMetadata(
bytes memory _metadata
) public virtual auth(UPDATE_METADATA_PERMISSION_ID) {
_updateMetadata(_metadata);
}

/// @notice Returns the metadata currently applied.
/// @return The The utf8 bytes of a content addressing cid.
function getMetadata() public view returns (bytes memory) {
MetadataExtensionStorage storage $ = _getMetadataExtensionStorage();
return $.metadata;
}

/// @notice Internal function to update metadata.
/// @param _metadata The utf8 bytes of a content addressing cid that stores contract's information.
function _updateMetadata(bytes memory _metadata) internal virtual {
MetadataExtensionStorage storage $ = _getMetadataExtensionStorage();
$.metadata = _metadata;

emit MetadataUpdated(_metadata);
}
}
121 changes: 121 additions & 0 deletions contracts/test/utils/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {
DAOMock,
DAOMock__factory,
MetadataExtensionMock__factory,
MetadataExtensionUpgradeableMock__factory,
MetadataExtensionMock,
MetadataExtensionUpgradeableMock,
} from '../../typechain';
import {erc165ComplianceTests} from '../helpers';
import {loadFixture} from '@nomicfoundation/hardhat-network-helpers';
import {expect} from 'chai';
import {ethers} from 'hardhat';

describe('MetadataExtension', async () => {
MetadataExtensionBaseTests(metadataFixture);
});

describe('MetadataExtensionUpgradeable', async () => {
MetadataExtensionBaseTests(metadataUpgradeableFixture);
});

// Contains tests for functionality common for `MetadataExtensionMock` and `MetadataExtensionMockUpgradeable` to avoid duplication.
function MetadataExtensionBaseTests(fixture: () => Promise<FixtureResult>) {
describe('ERC-165', async () => {
it('supports the `ERC-165` standard', async () => {
const {metadataMock} = await loadFixture(fixture);
const signers = await ethers.getSigners();
await erc165ComplianceTests(metadataMock, signers[0]);
});

it('supports the `updateMetadata/getMetadata` selector interface', async () => {
const {metadataMock} = await loadFixture(fixture);
const iface = MetadataExtensionMock__factory.createInterface();
const interfaceId = ethers.BigNumber.from(
iface.getSighash('updateMetadata')
)
.xor(ethers.BigNumber.from(iface.getSighash('getMetadata')))
.toHexString();

expect(await metadataMock.supportsInterface(interfaceId)).to.be.true;
});
});

describe('updateMetadata/getMetadata', async () => {
let data: FixtureResult;
beforeEach(async () => {
data = await loadFixture(fixture);
const {metadataMock, daoMock} = data;
await daoMock.setHasPermissionReturnValueMock(true);
});

it("reverts if caller doesn't have a permission", async () => {
const {metadataMock, daoMock} = data;
await daoMock.setHasPermissionReturnValueMock(false);

await expect(
metadataMock.updateMetadata('0x11')
).to.be.revertedWithCustomError(metadataMock, 'DaoUnauthorized');
});

it('sets the metadata and emits the event', async () => {
const {metadataMock} = data;
const metadata = '0x11';
await expect(metadataMock.updateMetadata(metadata))
.to.emit(metadataMock, 'MetadataUpdated')
.withArgs(metadata);
});

it('retrieves the metadata', async () => {
const {metadataMock} = data;
let metadata = '0x11';
await metadataMock.updateMetadata(metadata);
expect(await metadataMock.getMetadata()).to.equal(metadata);

// Check that it correctly retrieves the metadata if the length is > 32
// This ensures that our `sstore/sload` operations behave correctly.
metadata = '0x' + '11'.repeat(50);
await metadataMock.updateMetadata(metadata);
expect(await metadataMock.getMetadata()).to.equal(metadata);
});
});
}

type BaseFixtureResult = {
daoMock: DAOMock;
};

async function baseFixture(): Promise<BaseFixtureResult> {
const signers = await ethers.getSigners();
const daoMock = await new DAOMock__factory(signers[0]).deploy();

return {daoMock};
}

type FixtureResult = {
metadataMock: MetadataExtensionMock | MetadataExtensionUpgradeableMock;
daoMock: DAOMock;
};

async function metadataFixture(): Promise<FixtureResult> {
const {daoMock} = await baseFixture();
const signers = await ethers.getSigners();
const metadataMock = await new MetadataExtensionMock__factory(
signers[0]
).deploy(daoMock.address);

return {metadataMock, daoMock};
}

async function metadataUpgradeableFixture(): Promise<FixtureResult> {
const {daoMock} = await baseFixture();
const signers = await ethers.getSigners();

const metadataMock = await new MetadataExtensionUpgradeableMock__factory(
signers[0]
).deploy();

await metadataMock.initialize(daoMock.address);

return {metadataMock, daoMock};
}
Loading