Skip to content

Commit

Permalink
metadata contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
novaknole committed Oct 7, 2024
1 parent 2476aa2 commit cd22d48
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 0 deletions.
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 {MetadataContract} from "../../../../plugin/extensions/metadata/MetadataContract.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 MetadataContractMock is MetadataContract {
constructor(IDAO dao) DaoAuthorizable(dao) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.8;

import {MetadataContractUpgradeable} from "../../../../plugin/extensions/metadata/MetadataContractUpgradeable.sol";
import {IDAO} from "../../../../dao/IDAO.sol";

/// @notice A mock contract.
/// @dev DO NOT USE IN PRODUCTION!
contract MetadataContractUpgradeableMock is MetadataContractUpgradeable {
function initialize(IDAO _dao) public {
__DaoAuthorizableUpgradeable_init(_dao);
}
}
57 changes: 57 additions & 0 deletions contracts/src/plugin/extensions/metadata/MetadataContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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 MetadataContract
/// @author Aragon X - 2024
/// @custom:security-contact [email protected]
abstract contract MetadataContract 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);

/// @notice Thrown if metadata is set empty.
error EmptyMetadata();

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 {
if (_metadata.length == 0) {
revert EmptyMetadata();
}

metadata = _metadata;
emit MetadataUpdated(_metadata);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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 MetadataContract
/// @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 MetadataContractUpgradeable 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("osx-commons.storage.MetadataContractUpgradeable")
bytes32 private constant MetadataStorageLocation =
0x99da6c69991bd6a0d70d0c3817ab9bd9d4d7e3090d51c182be2cf851bfab8d70;

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

/// @notice Thrown if metadata is set empty.
error EmptyMetadata();

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

/// @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 {
if (_metadata.length == 0) {
revert EmptyMetadata();
}

_storeMetadata(_metadata);
emit MetadataUpdated(_metadata);
}

/// @notice Gets the currently set metadata.
/// @return _metadata The current metadata.
function _getMetadata() private view returns (bytes memory _metadata) {
assembly {
_metadata := sload(MetadataStorageLocation)
}
}

/// @notice Stores the metadata on a specific slot.
function _storeMetadata(bytes memory _metadata) private {
assembly {
sstore(MetadataStorageLocation, _metadata)
}
}
}
137 changes: 137 additions & 0 deletions contracts/test/plugin/extensions/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {
DAOMock,
DAOMock__factory,
IProposal__factory,
ProposalMock,
ProposalUpgradeableMock,
ProposalMock__factory,
ProposalUpgradeableMock__factory,
MetadataContractMock__factory,
MetadataContractUpgradeableMock__factory,
MetadataContractMock,
MetadataContractUpgradeableMock,
} from '../../../typechain';
import {MetadataContractUpgradeableInterface} from '../../../typechain/src/plugin/extensions/metadata/MetadataContractUpgradeable';
import {erc165ComplianceTests} from '../../helpers';
import {getInterfaceId} from '@aragon/osx-commons-sdk';
import {IProposal__factory as IProposal_V1_0_0__factory} from '@aragon/osx-ethers-v1.0.0';
import {loadFixture} from '@nomicfoundation/hardhat-network-helpers';
import {expect} from 'chai';
import {ethers} from 'hardhat';

describe.only('MetadataContract', async () => {
proposalBaseTests(metadataFixture);
});

describe('MetadataContractUpgradeable', async () => {
proposalBaseTests(metadataUpgradeableFixture);
});

// Contains tests for functionality common for `metadataMock` and `ProposalUpgradeableMock` to avoid duplication.
function proposalBaseTests(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 = MetadataContractMock__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('reverts if empty metadata is being set', async () => {
const {metadataMock} = data;

await expect(
metadataMock.updateMetadata('0x')
).to.be.revertedWithCustomError(metadataMock, 'EmptyMetadata');
});

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: MetadataContractMock | MetadataContractUpgradeableMock;
daoMock: DAOMock;
};

async function metadataFixture(): Promise<FixtureResult> {
const {daoMock} = await baseFixture();
const signers = await ethers.getSigners();
const metadataMock = await new MetadataContractMock__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 MetadataContractUpgradeableMock__factory(
signers[0]
).deploy();

await metadataMock.initialize(daoMock.address);

return {metadataMock, daoMock};
}

0 comments on commit cd22d48

Please sign in to comment.