Skip to content
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
4 changes: 3 additions & 1 deletion contracts/protocol/domain/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface FermionGeneralErrors {
error UnexpectedDataReturned(bytes data);
// Array elements that are not in ascending order (i.e arr[i-1] > arr[i])
error NonAscendingOrder();
error InvalidTokenId(address fnftAddress, uint256 tokenId);
error InvalidPeriod();
}

Expand Down Expand Up @@ -60,7 +61,6 @@ interface OfferErrors {
error InvalidRoyaltyRecipient(address recipient);
error InvalidRoyaltyPercentage(uint256 percentage);
error OfferWithoutRoyalties(uint256 offerId);
error InvalidTokenId(address fnftAddress, uint256 tokenId);
error InvalidCustomItemPrice();
}

Expand All @@ -75,6 +75,8 @@ interface VerificationErrors {
error PhygitalsAlreadyVerified(uint256 tokenId);
error PhygitalsDigestMismatch(uint256 tokenId, bytes32 expectedDigest, bytes32 actualDigest);
error PhygitalsVerificationMissing(uint256 tokenId);
error InexistentVerificationStatus();
error InvalidVerificationStatus();
}

interface CustodyErrors {
Expand Down
12 changes: 9 additions & 3 deletions contracts/protocol/domain/Types.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ contract FermionTypes {

enum VerificationStatus {
Verified,
Rejected
Rejected,
Pending
}

enum CheckoutRequestStatus {
Expand Down Expand Up @@ -70,6 +71,7 @@ contract FermionTypes {
CheckedOut,
Burned
}

enum PriceUpdateProposalState {
NotInit, // Explicitly represents an uninitialized state
Active,
Expand Down Expand Up @@ -97,6 +99,11 @@ contract FermionTypes {
bytes functionSignature;
}

struct Metadata {
string URI;
string hash;
}

struct Offer {
uint256 sellerId;
uint256 sellerDeposit;
Expand All @@ -108,8 +115,7 @@ contract FermionTypes {
uint256 facilitatorFeePercent;
address exchangeToken;
bool withPhygital;
string metadataURI;
string metadataHash;
Metadata metadata;
RoyaltyInfo royaltyInfo;
}

Expand Down
6 changes: 3 additions & 3 deletions contracts/protocol/facets/Offer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ contract OfferFacet is Context, OfferErrors, Access, FundsManager, IOfferEvents
bosonOffer.quantityAvailable = type(uint256).max; // unlimited offer
bosonOffer.exchangeToken = _offer.exchangeToken;
bosonOffer.priceType = IBosonProtocol.PriceType.Discovery;
bosonOffer.metadataUri = _offer.metadataURI;
bosonOffer.metadataHash = _offer.metadataHash;
bosonOffer.metadataUri = _offer.metadata.URI;
bosonOffer.metadataHash = _offer.metadata.hash;
bosonOffer.royaltyInfo = new IBosonProtocol.RoyaltyInfo[](1);
// bosonOffer.voided and bosonOffer.collectionIndex are not set, the defaults are fine

Expand Down Expand Up @@ -775,7 +775,7 @@ contract OfferFacet is Context, OfferErrors, Access, FundsManager, IOfferEvents

FermionTypes.Offer storage offer = FermionStorage.protocolEntities().offer[_offerId];
_exchangeToken = offer.exchangeToken;
wrapperAddress.initialize(address(_bosonVoucher), msgSender, _exchangeToken, _offerId, offer.metadataURI);
wrapperAddress.initialize(address(_bosonVoucher), msgSender, _exchangeToken, _offerId, offer.metadata.URI);
}

// wrap NFTs
Expand Down
6 changes: 3 additions & 3 deletions contracts/protocol/facets/Royalties.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.24;

import { OfferErrors } from "../domain/Errors.sol";
import { OfferErrors, FermionGeneralErrors } from "../domain/Errors.sol";
import { FermionTypes } from "../domain/Types.sol";
import { Access } from "../bases/mixins/Access.sol";
import { FermionStorage } from "../libs/Storage.sol";
Expand Down Expand Up @@ -129,13 +129,13 @@ contract RoyaltiesFacet is OfferErrors, Access {
address fermionFNFTAddress = FermionStorage.protocolLookups().offerLookups[offerId].fermionFNFTAddress;
if (fermionFNFTAddress == address(0)) {
// Token not preminted and wrapped yet
revert InvalidTokenId(fermionFNFTAddress, _tokenId);
revert FermionGeneralErrors.InvalidTokenId(fermionFNFTAddress, _tokenId);
} else if (fermionFNFTAddress != msg.sender) {
// This check is necessary only if the call is not from the FNFT contract, since that contract does the check anyway
try IERC721Metadata(fermionFNFTAddress).tokenURI(_tokenId) returns (string memory uri) {
// fermionFNFT will not return malformed URIs, so we can safely ignore the return value
} catch {
revert InvalidTokenId(fermionFNFTAddress, _tokenId);
revert FermionGeneralErrors.InvalidTokenId(fermionFNFTAddress, _tokenId);
}
}

Expand Down
66 changes: 61 additions & 5 deletions contracts/protocol/facets/Verification.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,15 @@ contract VerificationFacet is Context, Access, FundsManager, EIP712, Verificatio
*
* @param _tokenId - the token ID
* @param _verificationStatus - the verification status
* @param _verificationMetadata - optional verification metadata, with more information about the verification
*/
function submitVerdict(uint256 _tokenId, FermionTypes.VerificationStatus _verificationStatus) external {
function submitVerdict(
uint256 _tokenId,
FermionTypes.VerificationStatus _verificationStatus,
FermionTypes.Metadata calldata _verificationMetadata
) external {
getFundsAndPayVerifier(_tokenId, true);
FermionStorage.protocolLookups().tokenLookups[_tokenId].verificationMetadata = _verificationMetadata;
submitVerdictInternal(_tokenId, _verificationStatus, false);
}

Expand All @@ -74,10 +80,12 @@ contract VerificationFacet is Context, Access, FundsManager, EIP712, Verificatio
*
* @param _tokenId - the token ID
* @param _newMetadata - the uri of the new metadata
* @param _verificationMetadata - optional verification metadata, with more information about the verification
*/
function submitRevisedMetadata(
uint256 _tokenId,
string memory _newMetadata
string memory _newMetadata,
FermionTypes.Metadata calldata _verificationMetadata
) external notPaused(FermionTypes.PausableRegion.Verification) nonReentrant {
if (bytes(_newMetadata).length == 0) revert EmptyMetadata();

Expand All @@ -95,6 +103,7 @@ contract VerificationFacet is Context, Access, FundsManager, EIP712, Verificatio
);
}

tokenLookups.verificationMetadata = _verificationMetadata;
updateMetadataAndResetProposals(tokenLookups, _newMetadata, _tokenId);
}

Expand All @@ -110,10 +119,12 @@ contract VerificationFacet is Context, Access, FundsManager, EIP712, Verificatio
*
* @param _tokenId - the token ID
* @param _verificationStatus - the verification status
* @param _verificationMetadata - optional verification metadata, with more information about the verification
*/
function removeRevisedMetadataAndSubmitVerdict(
uint256 _tokenId,
FermionTypes.VerificationStatus _verificationStatus
FermionTypes.VerificationStatus _verificationStatus,
FermionTypes.Metadata calldata _verificationMetadata
) external notPaused(FermionTypes.PausableRegion.Verification) nonReentrant {
FermionStorage.TokenLookups storage tokenLookups = FermionStorage.protocolLookups().tokenLookups[_tokenId];

Expand All @@ -129,6 +140,7 @@ contract VerificationFacet is Context, Access, FundsManager, EIP712, Verificatio

updateMetadataAndResetProposals(tokenLookups, "", _tokenId);

tokenLookups.verificationMetadata = _verificationMetadata;
submitVerdictInternal(_tokenId, _verificationStatus, false);
}

Expand Down Expand Up @@ -274,6 +286,7 @@ contract VerificationFacet is Context, Access, FundsManager, EIP712, Verificatio
}
submitVerdictInternal(_tokenId, FermionTypes.VerificationStatus.Rejected, inactiveVerifier);
delete tokenLookups.revisedMetadata;
delete tokenLookups.verificationMetadata;
}

/**
Expand Down Expand Up @@ -343,6 +356,47 @@ contract VerificationFacet is Context, Access, FundsManager, EIP712, Verificatio
seller = tokenLookups.sellerSplitProposal;
}

/**
* @notice Get the verification status and metadata for a specific token
*
* @param _tokenId - the token ID
*
* @return verificationStatus - the verification status
* @return verificationMetadata - optional verification metadata, with more information about the verification
*/
function getVerificationDetails(
uint256 _tokenId
)
external
view
returns (FermionTypes.VerificationStatus verificationStatus, FermionTypes.Metadata memory verificationMetadata)
{
FermionStorage.ProtocolLookups storage pl = FermionStorage.protocolLookups();
(uint256 offerId, ) = FermionStorage.getOfferFromTokenId(_tokenId);

address fermionFNFTAddress = pl.offerLookups[offerId].fermionFNFTAddress;

if (fermionFNFTAddress == address(0)) {
revert FermionGeneralErrors.InvalidTokenId(fermionFNFTAddress, _tokenId);
}

FermionTypes.TokenState tokenState = IFermionFNFT(fermionFNFTAddress).tokenState(_tokenId);

if (tokenState < FermionTypes.TokenState.Unverified) {
revert InexistentVerificationStatus();
}

if (tokenState == FermionTypes.TokenState.Unverified) {
verificationStatus = FermionTypes.VerificationStatus.Pending;
} else if (tokenState == FermionTypes.TokenState.Burned) {
verificationStatus = FermionTypes.VerificationStatus.Rejected;
} else {
verificationStatus = FermionTypes.VerificationStatus.Verified;
}

verificationMetadata = pl.tokenLookups[_tokenId].verificationMetadata;
}

/**
* @notice Transfer the funds from Boson to Fermion and pay the verifier
*
Expand Down Expand Up @@ -421,6 +475,8 @@ contract VerificationFacet is Context, Access, FundsManager, EIP712, Verificatio
FermionTypes.VerificationStatus _verificationStatus,
bool _afterTimeout
) internal {
if (_verificationStatus == FermionTypes.VerificationStatus.Pending) revert InvalidVerificationStatus();

uint256 tokenId = _tokenId;
(uint256 offerId, FermionTypes.Offer storage offer) = FermionStorage.getOfferFromTokenId(tokenId);
uint256 verifierId = offer.verifierId;
Expand Down Expand Up @@ -498,11 +554,11 @@ contract VerificationFacet is Context, Access, FundsManager, EIP712, Verificatio
increaseAvailableFunds(buyerId, exchangeToken, remainder + sellerDeposit);

if (hasPhygitals) {
pl.tokenLookups[tokenId].phygitalsRecipient = 0; // reset phygitals verification status, so the seller can withdraw them
tokenLookups.phygitalsRecipient = 0; // reset phygitals verification status, so the seller can withdraw them
}
}

emit VerdictSubmitted(verifierId, tokenId, _verificationStatus);
emit VerdictSubmitted(verifierId, tokenId, _verificationStatus, tokenLookups.verificationMetadata);
}
}

Expand Down
3 changes: 2 additions & 1 deletion contracts/protocol/interfaces/events/IVerificationEvents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ interface IVerificationEvents {
event VerdictSubmitted(
uint256 indexed verifierId,
uint256 indexed nftId,
FermionTypes.VerificationStatus verificationStatus
FermionTypes.VerificationStatus verificationStatus,
FermionTypes.Metadata verificationMetadata
);
event RevisedMetadataSubmitted(uint256 indexed nftId, string newMetadata);
event ProposalSubmitted(uint256 indexed nftId, uint16 buyerProposal, uint16 sellerProposal, uint16 lastProposal);
Expand Down
2 changes: 2 additions & 0 deletions contracts/protocol/libs/Storage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ library FermionStorage {
FermionTypes.Phygital[] phygitals;
// phygitals recipient
uint256 phygitalsRecipient;
// verification metadata
FermionTypes.Metadata verificationMetadata;
// custom item price used only in case of forceful fractionalisation
uint256 selfSaleItemPrice;
}
Expand Down
42 changes: 27 additions & 15 deletions test/protocol/custodyFacet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "../utils/common";
import { expect } from "chai";
import { ethers } from "hardhat";
import { Contract, ZeroAddress, ZeroHash, parseEther } from "ethers";
import { Contract, ZeroAddress, ZeroHash, parseEther, id } from "ethers";
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
import {
EntityRole,
Expand Down Expand Up @@ -64,6 +64,11 @@ describe("Custody", function () {
const exchange = { tokenId: "", custodianId: "", price: parseEther("1.0") };
const exchangeSelfSale = { tokenId: "", custodianId: "" };
const exchangeSelfCustody = { tokenId: "", custodianId: "" };
const verificationMetadata = {
URI: "https://example.com/verification-metadata.json",
hash: id("metadata"),
};

const exchangeCustodianSwitch = { tokenId1: "", tokenId2: "", tokenId3: "", custodianId: "" };
let verifySellerAssistantRole: ReturnType<typeof verifySellerAssistantRoleClosure>;
let minimalPriceSelfSale: bigint;
Expand Down Expand Up @@ -108,8 +113,7 @@ describe("Custody", function () {
facilitatorFeePercent: "0",
exchangeToken: await mockToken.getAddress(),
withPhygital: false,
metadataURI: "https://example.com/offer-metadata.json",
metadataHash: ZeroHash,
metadata: { URI: "https://example.com/offer-metadata.json", hash: ZeroHash },
royaltyInfo: { recipients: [], bps: [] },
};

Expand Down Expand Up @@ -193,12 +197,20 @@ describe("Custody", function () {
exchangeCustodianSwitch.custodianId = custodianId;

// Submit verdicts
await verificationFacet.connect(verifier).submitVerdict(tokenId, VerificationStatus.Verified);
await verificationFacet.connect(verifier).submitVerdict(tokenIdSelf, VerificationStatus.Verified);
await verificationFacet.submitVerdict(tokenIdSelfCustody, VerificationStatus.Verified);
await verificationFacet.connect(verifier).submitVerdict(tokenIdCustodianSwitch, VerificationStatus.Verified);
await verificationFacet.connect(verifier).submitVerdict(tokenIdCustodianSwitch2, VerificationStatus.Verified);
await verificationFacet.connect(verifier).submitVerdict(tokenIdCustodianSwitch3, VerificationStatus.Verified);
await verificationFacet.connect(verifier).submitVerdict(tokenId, VerificationStatus.Verified, verificationMetadata);
await verificationFacet
.connect(verifier)
.submitVerdict(tokenIdSelf, VerificationStatus.Verified, verificationMetadata);
await verificationFacet.submitVerdict(tokenIdSelfCustody, VerificationStatus.Verified, verificationMetadata);
await verificationFacet
.connect(verifier)
.submitVerdict(tokenIdCustodianSwitch, VerificationStatus.Verified, verificationMetadata);
await verificationFacet
.connect(verifier)
.submitVerdict(tokenIdCustodianSwitch2, VerificationStatus.Verified, verificationMetadata);
await verificationFacet
.connect(verifier)
.submitVerdict(tokenIdCustodianSwitch3, VerificationStatus.Verified, verificationMetadata);

const wrapperAddress = await offerFacet.predictFermionFNFTAddress(offerId);
wrapper = await ethers.getContractAt("FermionFNFT", wrapperAddress);
Expand Down Expand Up @@ -370,7 +382,7 @@ describe("Custody", function () {
.to.be.revertedWithCustomError(wrapper, "InvalidStateOrCaller")
.withArgs(tokenId, fermionProtocolAddress, TokenState.Unverified);

await verificationFacet.submitVerdict(tokenId, VerificationStatus.Rejected);
await verificationFacet.submitVerdict(tokenId, VerificationStatus.Rejected, verificationMetadata);

// Unwrapped and rejected
await expect(custodyFacet.checkIn(tokenId))
Expand Down Expand Up @@ -543,7 +555,7 @@ describe("Custody", function () {
.to.be.revertedWithCustomError(fermionErrors, "InvalidCheckoutRequestStatus")
.withArgs(tokenId, CheckoutRequestStatus.CheckedIn, CheckoutRequestStatus.None);

await verificationFacet.submitVerdict(tokenId, VerificationStatus.Rejected);
await verificationFacet.submitVerdict(tokenId, VerificationStatus.Rejected, verificationMetadata);

// Unwrapped and rejected
await expect(custodyFacet.requestCheckOut(tokenId))
Expand Down Expand Up @@ -717,7 +729,7 @@ describe("Custody", function () {
.to.be.revertedWithCustomError(fermionErrors, "InvalidCheckoutRequestStatus")
.withArgs(tokenId, CheckoutRequestStatus.CheckOutRequested, CheckoutRequestStatus.None);

await verificationFacet.submitVerdict(tokenId, VerificationStatus.Rejected);
await verificationFacet.submitVerdict(tokenId, VerificationStatus.Rejected, verificationMetadata);

// Unwrapped and rejected
await expect(custodyFacet.submitTaxAmount(tokenId, taxAmount))
Expand Down Expand Up @@ -947,7 +959,7 @@ describe("Custody", function () {
.to.be.revertedWithCustomError(fermionErrors, "InvalidCheckoutRequestStatus")
.withArgs(tokenId, CheckoutRequestStatus.CheckOutRequested, CheckoutRequestStatus.None);

await verificationFacet.submitVerdict(tokenId, VerificationStatus.Rejected);
await verificationFacet.submitVerdict(tokenId, VerificationStatus.Rejected, verificationMetadata);

// Unwrapped and rejected
await expect(custodyFacet.connect(buyer).clearCheckoutRequest(tokenId))
Expand Down Expand Up @@ -1123,7 +1135,7 @@ describe("Custody", function () {
.to.be.revertedWithCustomError(fermionErrors, "InvalidCheckoutRequestStatus")
.withArgs(tokenId, CheckoutRequestStatus.CheckOutRequested, CheckoutRequestStatus.None);

await verificationFacet.submitVerdict(tokenId, VerificationStatus.Rejected);
await verificationFacet.submitVerdict(tokenId, VerificationStatus.Rejected, verificationMetadata);

// Unwrapped and rejected
await expect(custodyFacet.clearCheckoutRequest(tokenId))
Expand Down Expand Up @@ -1306,7 +1318,7 @@ describe("Custody", function () {
.to.be.revertedWithCustomError(fermionErrors, "InvalidCheckoutRequestStatus")
.withArgs(tokenId, CheckoutRequestStatus.CheckOutRequestCleared, CheckoutRequestStatus.None);

await verificationFacet.submitVerdict(tokenId, VerificationStatus.Rejected);
await verificationFacet.submitVerdict(tokenId, VerificationStatus.Rejected, verificationMetadata);

// Unwrapped and rejected
await expect(custodyFacet.checkOut(tokenId))
Expand Down
Loading
Loading