feat: contracts add verified field and allow verifier (#184)

* feat: add token verified flag

* feat: add verified flag on tokenURI

* feat: add isTokenVerified function

* test: add foundry tests for token verified

* test: add hardhat tests for token verified
This commit is contained in:
Felipe Mendes 2023-03-31 16:13:34 -03:00 committed by GitHub
parent 73d48a0e4f
commit 779cf3bb14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 210 additions and 9 deletions

View File

@ -51,6 +51,7 @@ contract FleekERC721 is
uint256 private _appIds;
mapping(uint256 => Token) private _apps;
mapping(uint256 => address) private _tokenVerifier;
mapping(uint256 => bool) private _tokenVerified;
/**
* @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
@ -133,6 +134,7 @@ contract FleekERC721 is
);
_tokenVerifier[tokenId] = verifier;
_tokenVerified[tokenId] = false;
_setAccessPointAutoApproval(tokenId, accessPointAutoApproval);
return tokenId;
@ -152,9 +154,10 @@ contract FleekERC721 is
_requireMinted(tokenId);
address owner = ownerOf(tokenId);
bool accessPointAutoApproval = _getAccessPointAutoApproval(tokenId);
bool verified = _tokenVerified[tokenId];
Token storage app = _apps[tokenId];
return string(abi.encodePacked(_baseURI(), app.toString(owner, accessPointAutoApproval).toBase64()));
return string(abi.encodePacked(_baseURI(), app.toString(owner, accessPointAutoApproval, verified).toBase64()));
}
/**
@ -437,6 +440,40 @@ contract FleekERC721 is
return _tokenVerifier[tokenId];
}
/**
* @dev Sets the verification status of a token.
*
* May emit a {MetadataUpdate} event.
*
* Requirements:
*
* - the tokenId must be minted and valid.
* - the sender must be the token verifier.
* - the sender must have `CollectionRoles.Verifier` role.
*
*/
function setTokenVerified(
uint256 tokenId,
bool verified
) public requireCollectionRole(CollectionRoles.Verifier) requireTokenVerifier(tokenId) {
_requireMinted(tokenId);
_tokenVerified[tokenId] = verified;
emit MetadataUpdate(tokenId, "verified", verified, msg.sender);
}
/**
* @dev Returns the verification status of a token.
*
* Requirements:
*
* - the tokenId must be minted and valid.
*
*/
function isTokenVerified(uint256 tokenId) public view returns (bool) {
_requireMinted(tokenId);
return _tokenVerified[tokenId];
}
/*//////////////////////////////////////////////////////////////
ACCESS POINTS
//////////////////////////////////////////////////////////////*/

View File

@ -36,7 +36,8 @@ library FleekStrings {
function toString(
IERCX.Token storage app,
address owner,
bool accessPointAutoApproval
bool accessPointAutoApproval,
bool verified
) internal view returns (string memory) {
// prettier-ignore
return string(abi.encodePacked(
@ -47,6 +48,7 @@ library FleekStrings {
'"external_url":"', app.externalURL, '",',
'"image":"', FleekSVG.generateBase64(app.name, app.ENS, app.logo, app.color.toColorString()), '",',
'"access_point_auto_approval":', accessPointAutoApproval.toString(),',',
'"verified":',verified.toString(),',',
'"attributes": [',
'{"trait_type": "ENS", "value":"', app.ENS,'"},',
'{"trait_type": "Commit Hash", "value":"', app.builds[app.currentBuild].commitHash,'"},',

View File

@ -594,6 +594,61 @@ contract Test_FleekERC721_AccessControl is Test_FleekERC721_Base, Test_FleekERC7
CuT.setPausable(true);
}
function test_setTokenVerifier() public {
address otherVerifier = address(0x1234);
CuT.grantCollectionRole(FleekAccessControl.CollectionRoles.Verifier, otherVerifier);
// ColletionOwner
vm.prank(collectionOwner);
expectRevertWithMustBeTokenOwner(tokenId);
CuT.setTokenVerifier(tokenId, otherVerifier);
// CollectionVerifier
vm.prank(collectionVerifier);
expectRevertWithMustBeTokenOwner(tokenId);
CuT.setTokenVerifier(tokenId, otherVerifier);
// TokenOwner
vm.prank(tokenOwner);
CuT.setTokenVerifier(tokenId, otherVerifier);
// TokenController
vm.prank(tokenController);
expectRevertWithMustBeTokenOwner(tokenId);
CuT.setTokenVerifier(tokenId, collectionVerifier);
// AnyAddress
vm.prank(anyAddress);
expectRevertWithMustBeTokenOwner(tokenId);
CuT.setTokenVerifier(tokenId, collectionVerifier);
}
function test_setTokenVerified() public {
// CollectionOwner
vm.prank(collectionOwner);
expectRevertWithCollectionRole(FleekAccessControl.CollectionRoles.Verifier);
CuT.setTokenVerified(tokenId, true);
// CollectionVerifier
vm.prank(collectionVerifier);
CuT.setTokenVerified(tokenId, true);
// TokenOwner
vm.prank(tokenOwner);
expectRevertWithCollectionRole(FleekAccessControl.CollectionRoles.Verifier);
CuT.setTokenVerified(tokenId, false);
// TokenController
vm.prank(tokenController);
expectRevertWithCollectionRole(FleekAccessControl.CollectionRoles.Verifier);
CuT.setTokenVerified(tokenId, false);
// AnyAddress
vm.prank(anyAddress);
expectRevertWithCollectionRole(FleekAccessControl.CollectionRoles.Verifier);
CuT.setTokenVerified(tokenId, false);
}
function test_cannotHaveLessThanOneCollectionOwner() public {
CuT.revokeCollectionRole(FleekAccessControl.CollectionRoles.Owner, collectionOwner);
expectRevertWithMustHaveAtLeastOneOwner();

View File

@ -7,10 +7,6 @@ import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
contract APConstants is Test {
using Strings for address;
function expectRevertWithMustBeTokenVerifier(uint256 tokenId) public {
vm.expectRevert(abi.encodeWithSelector(MustBeTokenVerifier.selector, tokenId));
}
function assertAccessPointJSON(
string memory accessPointName,
string memory _tokenId,

File diff suppressed because one or more lines are too long

View File

@ -38,6 +38,10 @@ abstract contract Test_FleekERC721_Assertions is Test {
function expectRevertWithInvalidTokenId() public {
vm.expectRevert("ERC721: invalid token ID");
}
function expectRevertWithMustBeTokenVerifier(uint256 tokenId) public {
vm.expectRevert(abi.encodeWithSelector(MustBeTokenVerifier.selector, tokenId));
}
}
abstract contract Test_FleekERC721_Base is Test, Test_FleekERC721_Assertions {

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "./TestBase.sol";
import {TestConstants} from "./Constants.sol";
import {FleekAccessControl} from "../../../contracts/FleekAccessControl.sol";
contract Test_FleekERC721_Verified is Test_FleekERC721_Base {
uint256 internal tokenId;
function setUp() public {
baseSetUp();
tokenId = mintDefault(deployer);
}
function test_shouldNotBeVerifiedAfterMint() public {
assertFalse(CuT.isTokenVerified(tokenId));
}
function test_shouldVerifyToken() public {
CuT.setTokenVerified(tokenId, true);
assertTrue(CuT.isTokenVerified(tokenId));
}
function test_verifyAndUnverify() public {
CuT.setTokenVerified(tokenId, true);
assertTrue(CuT.isTokenVerified(tokenId));
CuT.setTokenVerified(tokenId, false);
assertFalse(CuT.isTokenVerified(tokenId));
}
function testFuzz_shouldNotAllowVerifyIfHasNotVerifierRole(address verifier) public {
vm.assume(!CuT.hasCollectionRole(FleekAccessControl.CollectionRoles.Verifier, verifier));
vm.prank(verifier);
expectRevertWithCollectionRole(FleekAccessControl.CollectionRoles.Verifier);
CuT.setTokenVerified(tokenId, true);
}
function testFuzz_shouldNotAllowVerifyIfIsNotTokenVerifier(address verifier) public {
vm.assume(CuT.getTokenVerifier(tokenId) != verifier);
CuT.grantCollectionRole(FleekAccessControl.CollectionRoles.Verifier, verifier);
vm.prank(verifier);
expectRevertWithMustBeTokenVerifier(tokenId);
CuT.setTokenVerified(tokenId, true);
}
}

View File

@ -3,6 +3,7 @@ export const Errors = Object.freeze({
AccessPointAlreadyExists: 'AccessPointAlreadyExists',
AccessPointScoreCannotBeLower: 'AccessPointScoreCannotBeLower',
MustBeAccessPointOwner: 'MustBeAccessPointOwner',
MustBeTokenVerifier: 'MustBeTokenVerifier',
MustHaveCollectionRole: 'MustHaveCollectionRole',
MustHaveTokenRole: 'MustHaveTokenRole',
MustHaveAtLeastOneOwner: 'MustHaveAtLeastOneOwner',

View File

@ -22,6 +22,7 @@ describe('FleekERC721.TokenURI', () => {
image: TestConstants.ResultantImage.Default,
external_url: TestConstants.MintParams.externalUrl,
access_point_auto_approval: false,
verified: false,
attributes: [
{
trait_type: 'ENS',

View File

@ -0,0 +1,55 @@
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers';
import { expect } from 'chai';
import { Errors, Fixtures, TestConstants } from './helpers';
describe('FleekERC721.GetToken', () => {
let fixture: Awaited<ReturnType<typeof Fixtures.withMint>>;
beforeEach(async () => {
fixture = await loadFixture(Fixtures.withMint);
});
it('should mint token in not verified state', async () => {
const { contract, tokenId } = fixture;
expect(await contract.isTokenVerified(tokenId)).to.be.false;
});
it('should set token to verified state', async () => {
const { contract, tokenId } = fixture;
await contract.setTokenVerified(tokenId, true);
expect(await contract.isTokenVerified(tokenId)).to.be.true;
});
it('should set token to verified and unverified states', async () => {
const { contract, tokenId } = fixture;
await contract.setTokenVerified(tokenId, true);
await contract.setTokenVerified(tokenId, false);
expect(await contract.isTokenVerified(tokenId)).to.be.false;
});
it('should revert for non verifier call', async () => {
const { contract, tokenId, otherAccount } = fixture;
await expect(contract.connect(otherAccount).setTokenVerified(tokenId, true))
.to.be.revertedWithCustomError(contract, Errors.MustHaveCollectionRole)
.withArgs(TestConstants.CollectionRoles.Verifier);
});
it('should revert for non token verifier', async () => {
const { contract, tokenId, otherAccount } = fixture;
await contract.grantCollectionRole(
TestConstants.CollectionRoles.Verifier,
otherAccount.address
);
await expect(contract.connect(otherAccount).setTokenVerified(tokenId, true))
.to.be.revertedWithCustomError(contract, Errors.MustBeTokenVerifier)
.withArgs(tokenId);
});
});