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

feat: transferable doc #153

Merged
merged 6 commits into from
Mar 18, 2024
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
15 changes: 15 additions & 0 deletions src/OwnableDocumentStore.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity >=0.8.23 <0.9.0;

import "./base/BaseOwnableDocumentStore.sol";

contract OwnableDocumentStore is BaseOwnableDocumentStore {
constructor(string memory name_, string memory symbol_, address initAdmin) {
initialize(name_, symbol_, initAdmin);
}

function initialize(string memory name_, string memory symbol_, address initAdmin) internal initializer {
__OwnableDocumentStore_init(name_, symbol_, initAdmin);
}
}
163 changes: 163 additions & 0 deletions src/base/BaseOwnableDocumentStore.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity >=0.8.23 <0.9.0;

import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";

import "../base/DocumentStoreAccessControl.sol";
import "../interfaces/IOwnableDocumentStore.sol";
import "../interfaces/IOwnableDocumentStoreErrors.sol";
import "../interfaces/IERC5192.sol";

abstract contract BaseOwnableDocumentStore is
DocumentStoreAccessControl,
ERC721Upgradeable,
IERC5192,
IOwnableDocumentStoreErrors,
IOwnableDocumentStore
{
using Strings for uint256;

/// @custom:storage-location erc7201:openattestation.storage.OwnableDocumentStore
struct DocumentStoreStorage {
string baseURI;
mapping(uint256 => bool) revoked;
mapping(uint256 => bool) locked;
}

// keccak256(abi.encode(uint256(keccak256("openattestation.storage.OwnableDocumentStore")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant _DocumentStoreStorageSlot =
0x5b868bb5de5c3e5f8f786d02cbc568987b1921539a10331babbe7311c24de500;

function __OwnableDocumentStore_init(
string memory name_,
string memory symbol_,
address initAdmin
) internal onlyInitializing {
__ERC721_init(name_, symbol_);
__DocumentStoreAccessControl_init(initAdmin);
}

function name() public view override(IDocumentStore, ERC721Upgradeable) returns (string memory) {
return super.name();
}

function isActive(bytes32 document) public view nonZeroDocument(document) returns (bool) {
uint256 tokenId = uint256(document);
address owner = _ownerOf(tokenId);
if (owner == address(0) && _isRevoked(tokenId)) {
return false;
}
if (owner != address(0) && !_isRevoked(tokenId)) {
return true;
}
revert ERC721NonexistentToken(tokenId);
}

function issue(address to, bytes32 document, bool lock) public nonZeroDocument(document) onlyRole(ISSUER_ROLE) {
uint256 tokenId = uint256(document);
if (!_isRevoked(tokenId)) {
_mint(to, tokenId);
if (lock) {
_getStorage().locked[tokenId] = true;
emit Locked(tokenId);
} else {
emit Unlocked(tokenId);
}
} else {
revert DocumentIsRevoked(document);
}
}

function revoke(bytes32 document) public onlyRole(REVOKER_ROLE) {
uint256 tokenId = uint256(document);
_burn(tokenId);
_getStorage().revoked[tokenId] = true;
}

function isIssued(bytes32 document) public view nonZeroDocument(document) returns (bool) {
uint256 tokenId = uint256(document);
address owner = _ownerOf(tokenId);
if (owner != address(0) || _isRevoked(tokenId)) {
return true;
}
return false;
}

function isRevoked(bytes32 document) public view nonZeroDocument(document) returns (bool) {
uint256 tokenId = uint256(document);
address owner = _ownerOf(tokenId);
if (owner == address(0)) {
if (_isRevoked(tokenId)) {
return true;
}
revert ERC721NonexistentToken(tokenId);
}
return false;
}

function tokenURI(uint256 tokenId) public view override returns (string memory) {
_requireOwned(tokenId);

string memory tokenIdHexStr = tokenId.toHexString();

string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string.concat(baseURI, tokenIdHexStr) : "";
}

function supportsInterface(
bytes4 interfaceId
) public view virtual override(ERC721Upgradeable, AccessControlUpgradeable) returns (bool) {
return
interfaceId == type(IDocumentStore).interfaceId ||
interfaceId == type(IOwnableDocumentStore).interfaceId ||
interfaceId == type(IERC5192).interfaceId ||
super.supportsInterface(interfaceId);
}

function locked(uint256 tokenId) public view returns (bool) {
if (tokenId == 0) {
revert ZeroDocument();
}
return _isLocked(tokenId);
}

function setBaseURI(string memory baseURI) public onlyRole(DEFAULT_ADMIN_ROLE) {
_getStorage().baseURI = baseURI;
}

function _isRevoked(uint256 tokenId) internal view returns (bool) {
return _getStorage().revoked[tokenId];
}

function _isLocked(uint256 tokenId) internal view returns (bool) {
return _getStorage().locked[tokenId];
}

function _update(address to, uint256 tokenId, address auth) internal virtual override returns (address) {
address from = super._update(to, tokenId, auth);
if (_isLocked(tokenId) && (from != address(0) && to != address(0))) {
revert DocumentLocked(bytes32(tokenId));
}
return from;
}

function _baseURI() internal view virtual override returns (string memory) {
return _getStorage().baseURI;
}

function _getStorage() private pure returns (DocumentStoreStorage storage $) {
assembly {
$.slot := _DocumentStoreStorageSlot
}
}

modifier nonZeroDocument(bytes32 document) {
uint256 tokenId = uint256(document);
if (tokenId == 0) {
revert ZeroDocument();
}
_;
}
}
2 changes: 0 additions & 2 deletions src/interfaces/IDocumentStore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
pragma solidity >=0.8.23 <0.9.0;

interface IDocumentStore {


/**
* @notice Emitted when a document is issued
* @param document The hash of the issued document
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/IDocumentStoreBatchable.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.23 <0.9.0;

import "./IDocumentStore.sol";

interface IDocumentStoreBatchable is IDocumentStore {
Expand Down
20 changes: 20 additions & 0 deletions src/interfaces/IERC5192.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.23 <0.9.0;

interface IERC5192 {
/// @notice Emitted when the locking status is changed to locked.
/// @dev If a token is minted and the status is locked, this event should be emitted.
/// @param tokenId The identifier for a token.
event Locked(uint256 tokenId);

/// @notice Emitted when the locking status is changed to unlocked.
/// @dev If a token is minted and the status is unlocked, this event should be emitted.
/// @param tokenId The identifier for a token.
event Unlocked(uint256 tokenId);

/// @notice Returns the locking status of an Soulbound Token
/// @dev SBTs assigned to zero address are considered invalid, and queries
/// about them do throw.
/// @param tokenId The identifier for an SBT.
function locked(uint256 tokenId) external view returns (bool);
}
8 changes: 8 additions & 0 deletions src/interfaces/IOwnableDocumentStore.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.23 <0.9.0;

import "./IDocumentStore.sol";

interface IOwnableDocumentStore is IDocumentStore {
function issue(address to, bytes32 documentRoot, bool locked) external;
}
14 changes: 14 additions & 0 deletions src/interfaces/IOwnableDocumentStoreErrors.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.23 <0.9.0;

interface IOwnableDocumentStoreErrors {
error InactiveDocument(bytes32 document);

error DocumentExists(bytes32 document);

error ZeroDocument();

error DocumentIsRevoked(bytes32 document);

error DocumentLocked(bytes32 document);
}
31 changes: 27 additions & 4 deletions test/CommonTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "forge-std/Test.sol";
import "../src/DocumentStore.sol";
import "./fixtures/DocumentStoreFixture.sol";
import "../src/OwnableDocumentStore.sol";
import "./fixtures/OwnableDocumentStoreFixture.sol";

abstract contract CommonTest is Test {
address public owner = vm.addr(1);
Expand Down Expand Up @@ -152,7 +153,9 @@ abstract contract DocumentStore_multicall_revoke_Base is DocumentStoreCommonTest
bulkRevokeData[1] = abi.encodeCall(IDocumentStoreBatchable.revoke, (docRoots()[0], documents()[0], proofs()[0]));

// It should revert that document0 is already inactive
vm.expectRevert(abi.encodeWithSelector(IDocumentStoreErrors.InactiveDocument.selector, docRoots()[0], documents()[0]));
vm.expectRevert(
abi.encodeWithSelector(IDocumentStoreErrors.InactiveDocument.selector, docRoots()[0], documents()[0])
);

vm.prank(revoker);
documentStore.multicall(bulkRevokeData);
Expand Down Expand Up @@ -239,17 +242,37 @@ abstract contract OwnableDocumentStoreCommonTest is CommonTest {
string public storeName = "OwnableDocumentStore Test";
string public storeSymbol = "XYZ";

address public recipient = vm.addr(4);

function setUp() public virtual override {
super.setUp();

vm.startPrank(owner);

documentStore = new OwnableDocumentStore(storeName, storeSymbol, owner);
documentStore.grantRole(documentStore.ISSUER_ROLE(), issuer);
documentStore.grantRole(documentStore.REVOKER_ROLE(), revoker);
vm.stopPrank();
}
}

abstract contract OwnableDocumentStore_Initializer is OwnableDocumentStoreCommonTest {
OwnableDocumentStoreFixture private _fixture;
address[] public recipients;

function setUp() public virtual override {
super.setUp();

_fixture = new OwnableDocumentStoreFixture();

recipients = new address[](2);
recipients[0] = vm.addr(4);
recipients[1] = vm.addr(5);

vm.startPrank(issuer);
documentStore.issue(recipients[0], documents()[0], false);
documentStore.issue(recipients[1], documents()[1], true);
vm.stopPrank();
}

function documents() public view virtual returns (bytes32[] memory) {
return _fixture.documents();
}
}
Loading
Loading