-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
299 additions
and
0 deletions.
There are no files selected for viewing
13 changes: 13 additions & 0 deletions
13
contracts/src/mocks/plugin/extensions/metadata/MetadataContractMock.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) {} | ||
} |
14 changes: 14 additions & 0 deletions
14
contracts/src/mocks/plugin/extensions/metadata/MetadataContractUpgradeableMock.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
57
contracts/src/plugin/extensions/metadata/MetadataContract.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
78 changes: 78 additions & 0 deletions
78
contracts/src/plugin/extensions/metadata/MetadataContractUpgradeable.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}; | ||
} |