From dd56c83383ad3a758af012c7a37fd750c11c1ce6 Mon Sep 17 00:00:00 2001 From: Quentin <44470601+quent043@users.noreply.github.com> Date: Sat, 25 Nov 2023 09:49:30 +0000 Subject: [PATCH] Events upgrade (#200) * Added proposalId to Escrow & Review parmas Archived V1 contracts Signed-off-by: Quentin D.C * Added proposalId to Escrow & Review parmas Archived V1 contracts Signed-off-by: Quentin D.C * Updated payment events Signed-off-by: Quentin D.C * Added review "Mint" event test Signed-off-by: Quentin D.C * added arbitratorAdded and arbitratorRemoved events * Fixed imports Signed-off-by: Quentin D.C * Removes event in initializer Signed-off-by: Quentin D.C * Added PlatformId archive Signed-off-by: Quentin D.C * Renamed archive contract Signed-off-by: Quentin D.C * Removed unused import Signed-off-by: Quentin D.C * Updated RPC Signed-off-by: Quentin D.C * Added event Payment emit in tests Signed-off-by: Quentin D.C * Fixed test error Signed-off-by: Quentin D.C --------- Signed-off-by: Quentin D.C Co-authored-by: Yash Goyal --- contracts/TalentLayerEscrow.sol | 13 +- contracts/TalentLayerPlatformID.sol | 15 + contracts/TalentLayerReview.sol | 8 +- contracts/archive/TalentLayerEscrowV1.sol | 1045 +++++++++++++++++ contracts/archive/TalentLayerPlatformIdV1.sol | 694 +++++++++++ contracts/archive/TalentLayerReviewV1.sol | 282 +++++ .../interfaces/ITalentLayerPlatformID.sol | 4 + hardhat.config.ts | 3 +- scripts/playground/0-mint-platform-ID.ts | 1 - test/batch/disputeResolution.ts | 12 +- test/batch/fullWorkflow.ts | 50 +- 11 files changed, 2114 insertions(+), 13 deletions(-) create mode 100644 contracts/archive/TalentLayerEscrowV1.sol create mode 100644 contracts/archive/TalentLayerPlatformIdV1.sol create mode 100644 contracts/archive/TalentLayerReviewV1.sol diff --git a/contracts/TalentLayerEscrow.sol b/contracts/TalentLayerEscrow.sol index 77578756..a3ac1f70 100644 --- a/contracts/TalentLayerEscrow.sol +++ b/contracts/TalentLayerEscrow.sol @@ -120,13 +120,15 @@ contract TalentLayerEscrow is * @param _token The address of the token used for the payment. * @param _amount The amount paid. * @param _serviceId The id of the concerned service. + * @param _proposalId The id of the corresponding proposal. */ event Payment( uint256 _transactionId, PaymentType _paymentType, address _token, uint256 _amount, - uint256 _serviceId + uint256 _serviceId, + uint256 _proposalId ); /** @@ -980,7 +982,14 @@ contract TalentLayerEscrow is */ function _afterPayment(uint256 _transactionId, PaymentType _paymentType, uint256 _releaseAmount) private { Transaction storage transaction = transactions[_transactionId]; - emit Payment(transaction.id, _paymentType, transaction.token, _releaseAmount, transaction.serviceId); + emit Payment( + transaction.id, + _paymentType, + transaction.token, + _releaseAmount, + transaction.serviceId, + transaction.proposalId + ); if (transaction.amount == 0) { talentLayerServiceContract.afterFullPayment(transaction.serviceId, transaction.releasedAmount); diff --git a/contracts/TalentLayerPlatformID.sol b/contracts/TalentLayerPlatformID.sol index 710b90f5..6a72ebea 100644 --- a/contracts/TalentLayerPlatformID.sol +++ b/contracts/TalentLayerPlatformID.sol @@ -419,6 +419,7 @@ contract TalentLayerPlatformID is ERC721Upgradeable, AccessControlUpgradeable, U function addArbitrator(address _arbitrator, bool _isInternal) public onlyRole(DEFAULT_ADMIN_ROLE) { validArbitrators[address(_arbitrator)] = true; internalArbitrators[address(_arbitrator)] = _isInternal; + emit ArbitratorAdded(_arbitrator, _isInternal); } /** @@ -429,6 +430,7 @@ contract TalentLayerPlatformID is ERC721Upgradeable, AccessControlUpgradeable, U function removeArbitrator(address _arbitrator) public onlyRole(DEFAULT_ADMIN_ROLE) { validArbitrators[address(_arbitrator)] = false; internalArbitrators[address(_arbitrator)] = false; + emit ArbitratorRemoved(_arbitrator); } /** @@ -638,6 +640,19 @@ contract TalentLayerPlatformID is ERC721Upgradeable, AccessControlUpgradeable, U */ event OriginValidatedProposalFeeRateUpdated(uint256 platformId, uint16 originValidatedProposalFeeRate); + /** + * @notice Emit after the arbitrator is added + * @param arbitrator The address of the new arbitrator + * @param isInternal Boolean denoting if the arbitrator is internal (is part of TalentLayer) or not + */ + event ArbitratorAdded(address arbitrator, bool isInternal); + + /** + * @notice Emit after the arbitrator is removed + * @param arbitrator The address of the arbitrator + */ + event ArbitratorRemoved(address arbitrator); + /** * @notice Emit after the arbitrator is updated for a platform * @param platformId The Platform Id diff --git a/contracts/TalentLayerReview.sol b/contracts/TalentLayerReview.sol index c6d4bb19..d19f2401 100644 --- a/contracts/TalentLayerReview.sol +++ b/contracts/TalentLayerReview.sol @@ -165,6 +165,8 @@ contract TalentLayerReview is ERC2771RecipientUpgradeable, ERC721Upgradeable, UU uint256 reviewId = nextReviewId.current(); nextReviewId.increment(); + ITalentLayerService.Service memory service = talentLayerService.getService(_serviceId); + reviews[reviewId] = Review({ id: reviewId, ownerId: _to, @@ -173,7 +175,7 @@ contract TalentLayerReview is ERC2771RecipientUpgradeable, ERC721Upgradeable, UU rating: _rating }); - emit Mint(_serviceId, _to, reviewId, _rating, _reviewUri); + emit Mint(_serviceId, _to, reviewId, _rating, _reviewUri, service.acceptedProposalId); return reviewId; } @@ -271,12 +273,14 @@ contract TalentLayerReview is ERC2771RecipientUpgradeable, ERC721Upgradeable, UU * @param tokenId The ID of the review token * @param rating The rating of the review * @param reviewUri The IPFS URI of the review metadata + * @param proposalId The id of the corresponding proposal. */ event Mint( uint256 indexed serviceId, uint256 indexed toId, uint256 indexed tokenId, uint256 rating, - string reviewUri + string reviewUri, + uint256 proposalId ); } diff --git a/contracts/archive/TalentLayerEscrowV1.sol b/contracts/archive/TalentLayerEscrowV1.sol new file mode 100644 index 00000000..fdf3faf4 --- /dev/null +++ b/contracts/archive/TalentLayerEscrowV1.sol @@ -0,0 +1,1045 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {ITalentLayerService} from "../interfaces/ITalentLayerService.sol"; +import {ITalentLayerID} from "../interfaces/ITalentLayerID.sol"; +import {ITalentLayerPlatformID} from "../interfaces/ITalentLayerPlatformID.sol"; +import "../libs/ERC2771RecipientUpgradeable.sol"; +import {IArbitrable} from "../interfaces/IArbitrable.sol"; +import {Arbitrator} from "../Arbitrator.sol"; + +/** + * @title TalentLayer Escrow Contract + * @author TalentLayer Team | Website: https://talentlayer.org | Twitter: @talentlayer + */ +contract TalentLayerEscrowV1 is + Initializable, + ERC2771RecipientUpgradeable, + PausableUpgradeable, + UUPSUpgradeable, + IArbitrable +{ + using CountersUpgradeable for CountersUpgradeable.Counter; + using SafeERC20Upgradeable for IERC20Upgradeable; + + // =========================== Enum ============================== + + /** + * @notice Enum payment type + */ + enum PaymentType { + Release, + Reimburse + } + + /** + * @notice Arbitration fee payment type enum + */ + enum ArbitrationFeePaymentType { + Pay, + Reimburse + } + + /** + * @notice party type enum + */ + enum Party { + Sender, + Receiver + } + + /** + * @notice Transaction status enum + */ + enum Status { + NoDispute, // no dispute has arisen about the transaction + WaitingSender, // receiver has paid arbitration fee, while sender still has to do it + WaitingReceiver, // sender has paid arbitration fee, while receiver still has to do it + DisputeCreated, // both parties have paid the arbitration fee and a dispute has been created + Resolved // the transaction is solved (either no dispute has ever arisen or the dispute has been resolved) + } + + // =========================== Struct ============================== + + /** + * @notice Transaction struct + * @param id Incremental identifier + * @param sender The party paying the escrow amount + * @param receiver The intended receiver of the escrow amount + * @param token The token used for the transaction + * @param amount The amount of the transaction EXCLUDING FEES + * @param releasedAmount The amount of the transaction that has been released to the receiver EXCLUDING FEES + * @param serviceId The ID of the associated service + * @param proposalId The id of the validated proposal + * @param protocolEscrowFeeRate The %fee (per ten thousands) paid to the protocol's owner + * @param originServiceFeeRate The %fee (per ten thousands) paid to the platform on which the service was created + * @param originValidatedProposalFeeRate the %fee (per ten thousands) paid to the platform on which the proposal was validated + * @param arbitrator The address of the contract that can rule on a dispute for the transaction. + * @param status The status of the transaction for the dispute procedure. + * @param disputeId The ID of the dispute, if it exists + * @param senderFee Total fees paid by the sender for the dispute procedure. + * @param receiverFee Total fees paid by the receiver for the dispute procedure. + * @param lastInteraction Last interaction for the dispute procedure. + * @param arbitratorExtraData Extra data to set up the arbitration. + * @param arbitrationFeeTimeout timeout for parties to pay the arbitration fee + */ + struct Transaction { + uint256 id; + address sender; + address receiver; + address token; + uint256 amount; + uint256 releasedAmount; + uint256 serviceId; + uint256 proposalId; + uint16 protocolEscrowFeeRate; + uint16 originServiceFeeRate; + uint16 originValidatedProposalFeeRate; + Arbitrator arbitrator; + Status status; + uint256 disputeId; + uint256 senderFee; + uint256 receiverFee; + uint256 lastInteraction; + bytes arbitratorExtraData; + uint256 arbitrationFeeTimeout; + } + + // =========================== Events ============================== + + /** + * @notice Emitted after each payment + * @param _transactionId The id of the transaction. + * @param _paymentType Whether the payment is a release or a reimbursement. + * @param _token The address of the token used for the payment. + * @param _amount The amount paid. + * @param _serviceId The id of the concerned service. + */ + event Payment( + uint256 _transactionId, + PaymentType _paymentType, + address _token, + uint256 _amount, + uint256 _serviceId + ); + + /** + * @notice Emitted after the total amount of a transaction has been paid. At this moment the service is considered finished. + * @param _serviceId The service ID + */ + event PaymentCompleted(uint256 _serviceId); + + /** + * @notice Emitted after the protocol fee was updated + * @param _protocolEscrowFeeRate The new protocol fee + */ + event ProtocolEscrowFeeRateUpdated(uint16 _protocolEscrowFeeRate); + + /** + * @notice Emitted after a platform withdraws its balance + * @param _platformId The Platform ID to which the balance is transferred. + * @param _token The address of the token used for the payment. + * @param _amount The amount transferred. + */ + event FeesClaimed(uint256 _platformId, address indexed _token, uint256 _amount); + + /** + * @notice Emitted after an origin service fee is released to a platform's balance + * @param _platformId The platform ID. + * @param _serviceId The related service ID. + * @param _token The address of the token used for the payment. + * @param _amount The amount released. + */ + event OriginServiceFeeRateReleased( + uint256 _platformId, + uint256 _serviceId, + address indexed _token, + uint256 _amount + ); + + /** + * @notice Emitted after an origin service fee is released to a platform's balance + * @param _platformId The platform ID. + * @param _serviceId The related service ID. + * @param _token The address of the token used for the payment. + * @param _amount The amount released. + */ + event OriginValidatedProposalFeeRateReleased( + uint256 _platformId, + uint256 _serviceId, + address indexed _token, + uint256 _amount + ); + + /** + * @notice Emitted when a party has to pay a fee for the dispute or would otherwise be considered as losing. + * @param _transactionId The id of the transaction. + * @param _party The party who has to pay. + */ + event HasToPayFee(uint256 indexed _transactionId, Party _party); + + /** + * @notice Emitted when a party either pays the arbitration fee or gets it reimbursed. + * @param _transactionId The id of the transaction. + * @param _paymentType Whether the party paid or got reimbursed. + * @param _party The party who has paid/got reimbursed the fee. + * @param _amount The amount paid/reimbursed + */ + event ArbitrationFeePayment( + uint256 indexed _transactionId, + ArbitrationFeePaymentType _paymentType, + Party _party, + uint256 _amount + ); + + /** + * @notice Emitted when a ruling is executed. + * @param _transactionId The index of the transaction. + * @param _ruling The given ruling. + */ + event RulingExecuted(uint256 indexed _transactionId, uint256 _ruling); + + /** + * @notice Emitted when a transaction is created. + * @param _transactionId Incremental idenfitifier + * @param _senderId The TL Id of the party paying the escrow amount + * @param _receiverId The TL Id of the intended receiver of the escrow amount + * @param _token The token used for the transaction + * @param _amount The amount of the transaction EXCLUDING FEES + * @param _serviceId The ID of the associated service + * @param _protocolEscrowFeeRate The %fee (per ten thousands) to pay to the protocol's owner + * @param _originServiceFeeRate The %fee (per ten thousands) to pay to the platform on which the transaction was created + * @param _originValidatedProposalFeeRate the %fee (per ten thousands) to pay to the platform on which the validated proposal was created + * @param _arbitrator The address of the contract that can rule on a dispute for the transaction. + * @param _arbitratorExtraData Extra data to set up the arbitration. + * @param _arbitrationFeeTimeout timeout for parties to pay the arbitration fee + */ + event TransactionCreated( + uint256 _transactionId, + uint256 _senderId, + uint256 _receiverId, + address _token, + uint256 _amount, + uint256 _serviceId, + uint256 _proposalId, + uint16 _protocolEscrowFeeRate, + uint16 _originServiceFeeRate, + uint16 _originValidatedProposalFeeRate, + Arbitrator _arbitrator, + bytes _arbitratorExtraData, + uint256 _arbitrationFeeTimeout + ); + + /** + * @notice Emitted when evidence is submitted. + * @param _transactionId The id of the transaction. + * @param _partyId The party submitting the evidence. + * @param _evidenceUri The URI of the evidence. + */ + event EvidenceSubmitted(uint256 indexed _transactionId, uint256 indexed _partyId, string _evidenceUri); + + // =========================== Declarations ============================== + + /** + * @notice Mapping from transactionId to Transactions + */ + mapping(uint256 => Transaction) private transactions; + + /** + * @notice Mapping from platformId to Token address to Token Balance + * Represents the amount of ETH or token present on this contract which + * belongs to a platform and can be withdrawn. + * @dev Id 0 (PROTOCOL_INDEX) is reserved to the protocol balance + * @dev address(0) is reserved to ETH balance + */ + mapping(uint256 => mapping(address => uint256)) private platformIdToTokenToBalance; + + /** + * @notice Instance of TalentLayerService.sol + */ + ITalentLayerService private talentLayerServiceContract; + + /** + * @notice Instance of TalentLayerID.sol + */ + ITalentLayerID private talentLayerIdContract; + + /** + * @notice Instance of TalentLayerPlatformID.sol + */ + ITalentLayerPlatformID private talentLayerPlatformIdContract; + + /** + * @notice (Upgradable) Wallet which will receive the protocol fees + */ + address payable public protocolWallet; + + /** + * @notice Percentage paid to the protocol (per 10,000, upgradable) + */ + uint16 public protocolEscrowFeeRate; + + /** + * @notice The index of the protocol in the "platformIdToTokenToBalance" mapping + */ + uint8 private constant PROTOCOL_INDEX = 0; + + /** + * @notice The fee divider used for every fee rates + */ + uint16 private constant FEE_DIVIDER = 10000; + + /** + * @notice Amount of choices available for ruling the disputes + */ + uint8 constant AMOUNT_OF_CHOICES = 2; + + /** + * @notice Ruling id for sender to win the dispute + */ + uint8 constant SENDER_WINS = 1; + + /** + * @notice Ruling id for receiver to win the dispute + */ + uint8 constant RECEIVER_WINS = 2; + + /** + * @notice One-to-one relationship between the dispute and the transaction. + */ + mapping(uint256 => uint256) public disputeIDtoTransactionID; + + /** + * @notice Platform Id counter + */ + CountersUpgradeable.Counter private nextTransactionId; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + // =========================== Modifiers ============================== + + /** + * @notice Check if the given address is either the owner or the delegate of the given user + * @param _profileId The TalentLayer ID of the user + */ + modifier onlyOwnerOrDelegate(uint256 _profileId) { + require(talentLayerIdContract.isOwnerOrDelegate(_profileId, _msgSender()), "Not owner or delegate"); + _; + } + + // =========================== Initializers ============================== + + /** + * @dev Called on contract deployment + * @param _talentLayerServiceAddress Contract address to TalentLayerService.sol + * @param _talentLayerIDAddress Contract address to TalentLayerID.sol + * @param _talentLayerPlatformIDAddress Contract address to TalentLayerPlatformID.sol + * @param _protocolWallet Wallet used to receive fees + */ + function initialize( + address _talentLayerServiceAddress, + address _talentLayerIDAddress, + address _talentLayerPlatformIDAddress, + address _protocolWallet + ) public initializer { + __Ownable_init(); + __UUPSUpgradeable_init(); + + talentLayerServiceContract = ITalentLayerService(_talentLayerServiceAddress); + talentLayerIdContract = ITalentLayerID(_talentLayerIDAddress); + talentLayerPlatformIdContract = ITalentLayerPlatformID(_talentLayerPlatformIDAddress); + protocolWallet = payable(_protocolWallet); + // Increment counter to start transaction ids at index 1 + nextTransactionId.increment(); + + updateProtocolEscrowFeeRate(100); + } + + // =========================== View functions ============================== + + /** + * @dev Only the owner of the platform ID or the owner can execute this function + * @param _token Token address ("0" for ETH) + * @return balance The balance of the platform or the protocol + */ + function getClaimableFeeBalance(address _token) external view returns (uint256 balance) { + address sender = _msgSender(); + + if (owner() == sender) { + return platformIdToTokenToBalance[PROTOCOL_INDEX][_token]; + } + uint256 platformId = talentLayerPlatformIdContract.ids(sender); + talentLayerPlatformIdContract.isValid(platformId); + return platformIdToTokenToBalance[platformId][_token]; + } + + /** + * @notice Called to get the details of a transaction + * @dev Only the transaction sender or receiver can call this function + * @param _transactionId Id of the transaction + * @return transaction The transaction details + */ + function getTransactionDetails(uint256 _transactionId) external view returns (Transaction memory) { + Transaction memory transaction = transactions[_transactionId]; + require(transaction.id < nextTransactionId.current(), "Invalid transaction id"); + + address sender = _msgSender(); + require( + sender == transaction.sender || sender == transaction.receiver, + "You are not related to this transaction" + ); + return transaction; + } + + // =========================== Owner functions ============================== + + /** + * @notice Updates the Protocol Fee rate + * @dev Only the owner can call this function + * @param _protocolEscrowFeeRate The new protocol fee + */ + function updateProtocolEscrowFeeRate(uint16 _protocolEscrowFeeRate) public onlyOwner { + protocolEscrowFeeRate = _protocolEscrowFeeRate; + emit ProtocolEscrowFeeRateUpdated(_protocolEscrowFeeRate); + } + + /** + * @notice Updates the Protocol wallet that receive fees + * @dev Only the owner can call this function + * @param _protocolWallet The new wallet address + */ + function updateProtocolWallet(address payable _protocolWallet) external onlyOwner { + protocolWallet = _protocolWallet; + } + + /** + * @dev Pauses the creation of transaction, releases and reimbursements. + */ + function pause() external onlyOwner { + _pause(); + } + + /** + * @dev Unpauses the creation of transaction, releases and reimbursements. + */ + function unpause() external onlyOwner { + _unpause(); + } + + // =========================== User functions ============================== + + /** + * @dev Validates a proposal for a service by locking token into escrow. + * @param _serviceId Id of the service that the sender created and the proposal was made for. + * @param _proposalId Id of the proposal that the transaction validates. + * @param _metaEvidence Link to the meta-evidence. + * @param _originDataUri dataURI of the validated proposal + */ + function createTransaction( + uint256 _serviceId, + uint256 _proposalId, + string memory _metaEvidence, + string memory _originDataUri + ) external payable whenNotPaused returns (uint256) { + ( + ITalentLayerService.Service memory service, + ITalentLayerService.Proposal memory proposal + ) = talentLayerServiceContract.getServiceAndProposal(_serviceId, _proposalId); + (address sender, address receiver) = talentLayerIdContract.ownersOf(service.ownerId, proposal.ownerId); + + ITalentLayerPlatformID.Platform memory originServiceCreationPlatform = talentLayerPlatformIdContract + .getPlatform(service.platformId); + ITalentLayerPlatformID.Platform memory originProposalCreationPlatform = service.platformId != + proposal.platformId + ? talentLayerPlatformIdContract.getPlatform(proposal.platformId) + : originServiceCreationPlatform; + + uint256 transactionAmount = _calculateTotalWithFees( + proposal.rateAmount, + originServiceCreationPlatform.originServiceFeeRate, + originProposalCreationPlatform.originValidatedProposalFeeRate + ); + + if (proposal.rateToken == address(0)) { + require(msg.value == transactionAmount, "Non-matching funds"); + } else { + require(msg.value == 0, "Non-matching funds"); + } + + require(_msgSender() == sender, "Access denied"); + require(proposal.ownerId == _proposalId, "Incorrect proposal ID"); + require(proposal.expirationDate >= block.timestamp, "Proposal expired"); + require(service.status == ITalentLayerService.Status.Opened, "Service status not open"); + require(proposal.status == ITalentLayerService.ProposalStatus.Pending, "Proposal status not pending"); + require(bytes(_metaEvidence).length == 46, "Invalid cid"); + require( + keccak256(abi.encodePacked(proposal.dataUri)) == keccak256(abi.encodePacked(_originDataUri)), + "Proposal dataUri has changed" + ); + + uint256 transactionId = nextTransactionId.current(); + transactions[transactionId] = Transaction({ + id: transactionId, + sender: sender, + receiver: receiver, + token: proposal.rateToken, + amount: proposal.rateAmount, + releasedAmount: 0, + serviceId: _serviceId, + proposalId: _proposalId, + protocolEscrowFeeRate: protocolEscrowFeeRate, + originServiceFeeRate: originServiceCreationPlatform.originServiceFeeRate, + originValidatedProposalFeeRate: originProposalCreationPlatform.originValidatedProposalFeeRate, + disputeId: 0, + senderFee: 0, + receiverFee: 0, + lastInteraction: block.timestamp, + status: Status.NoDispute, + arbitrator: originServiceCreationPlatform.arbitrator, + arbitratorExtraData: originServiceCreationPlatform.arbitratorExtraData, + arbitrationFeeTimeout: originServiceCreationPlatform.arbitrationFeeTimeout + }); + + nextTransactionId.increment(); + + talentLayerServiceContract.afterDeposit(_serviceId, _proposalId, transactionId); + + if (proposal.rateToken != address(0)) { + IERC20Upgradeable(proposal.rateToken).safeTransferFrom(sender, address(this), transactionAmount); + } + + _afterCreateTransaction(service.ownerId, proposal.ownerId, transactionId, _metaEvidence); + + return transactionId; + } + + /** + * @notice Allows the sender to release locked-in escrow value to the intended recipient. + * The amount released must not include the fees. + * @param _profileId The TalentLayer ID of the user + * @param _transactionId Id of the transaction to release escrow value for. + * @param _amount Value to be released without fees. Should not be more than amount locked in. + */ + function release( + uint256 _profileId, + uint256 _transactionId, + uint256 _amount + ) external whenNotPaused onlyOwnerOrDelegate(_profileId) { + _validatePayment(_transactionId, PaymentType.Release, _profileId, _amount); + + Transaction storage transaction = transactions[_transactionId]; + transaction.amount -= _amount; + transaction.releasedAmount += _amount; + + _release(_transactionId, _amount); + } + + /** + * @notice Allows the intended receiver to return locked-in escrow value back to the sender. + * The amount reimbursed must not include the fees. + * @param _profileId The TalentLayer ID of the user + * @param _transactionId Id of the transaction to reimburse escrow value for. + * @param _amount Value to be reimbursed without fees. Should not be more than amount locked in. + */ + function reimburse( + uint256 _profileId, + uint256 _transactionId, + uint256 _amount + ) external whenNotPaused onlyOwnerOrDelegate(_profileId) { + _validatePayment(_transactionId, PaymentType.Reimburse, _profileId, _amount); + + Transaction storage transaction = transactions[_transactionId]; + transaction.amount -= _amount; + + _reimburse(_transactionId, _amount); + } + + /** + * @notice Allows the sender of the transaction to pay the arbitration fee to raise a dispute. + * Note that the arbitrator can have createDispute throw, which will make this function throw and therefore lead to a party being timed-out. + * This is not a vulnerability as the arbitrator can rule in favor of one party anyway. + * @param _transactionId Id of the transaction. + */ + function payArbitrationFeeBySender(uint256 _transactionId) public payable { + Transaction storage transaction = transactions[_transactionId]; + + require(address(transaction.arbitrator) != address(0), "Arbitrator not set"); + require( + transaction.status < Status.DisputeCreated, + "Dispute has already been created or because the transaction has been executed" + ); + require(_msgSender() == transaction.sender, "The caller must be the sender"); + + uint256 arbitrationCost = transaction.arbitrator.arbitrationCost(transaction.arbitratorExtraData); + transaction.senderFee += msg.value; + // The total fees paid by the sender should be at least the arbitration cost. + require(transaction.senderFee == arbitrationCost, "The sender fee must be equal to the arbitration cost"); + + transaction.lastInteraction = block.timestamp; + + emit ArbitrationFeePayment(_transactionId, ArbitrationFeePaymentType.Pay, Party.Sender, msg.value); + + // The receiver still has to pay. This can also happen if he has paid, but arbitrationCost has increased. + if (transaction.receiverFee < arbitrationCost) { + transaction.status = Status.WaitingReceiver; + emit HasToPayFee(_transactionId, Party.Receiver); + } else { + // The receiver has also paid the fee. We create the dispute. + _raiseDispute(_transactionId, arbitrationCost); + } + } + + /** + * @notice Allows the receiver of the transaction to pay the arbitration fee to raise a dispute. + * Note that this function mirrors payArbitrationFeeBySender. + * @param _transactionId Id of the transaction. + */ + function payArbitrationFeeByReceiver(uint256 _transactionId) public payable { + Transaction storage transaction = transactions[_transactionId]; + + require(address(transaction.arbitrator) != address(0), "Arbitrator not set"); + require( + transaction.status < Status.DisputeCreated, + "Dispute has already been created or because the transaction has been executed" + ); + require(_msgSender() == transaction.receiver, "The caller must be the receiver"); + + uint256 arbitrationCost = transaction.arbitrator.arbitrationCost(transaction.arbitratorExtraData); + transaction.receiverFee += msg.value; + // The total fees paid by the receiver should be at least the arbitration cost. + require(transaction.receiverFee == arbitrationCost, "The receiver fee must be equal to the arbitration cost"); + + transaction.lastInteraction = block.timestamp; + + emit ArbitrationFeePayment(_transactionId, ArbitrationFeePaymentType.Pay, Party.Receiver, msg.value); + + // The sender still has to pay. This can also happen if he has paid, but arbitrationCost has increased. + if (transaction.senderFee < arbitrationCost) { + transaction.status = Status.WaitingSender; + emit HasToPayFee(_transactionId, Party.Sender); + } else { + // The sender has also paid the fee. We create the dispute. + _raiseDispute(_transactionId, arbitrationCost); + } + } + + /** + * @notice If one party fails to pay the arbitration fee in time, the other can call this function and will win the case + * @param _transactionId Id of the transaction. + */ + function arbitrationFeeTimeout(uint256 _transactionId) public { + Transaction storage transaction = transactions[_transactionId]; + + require( + block.timestamp - transaction.lastInteraction >= transaction.arbitrationFeeTimeout, + "Timeout time has not passed yet" + ); + + if (transaction.status == Status.WaitingSender) { + if (transaction.senderFee != 0) { + uint256 senderFee = transaction.senderFee; + transaction.senderFee = 0; + payable(transaction.sender).call{value: senderFee}(""); + } + _executeRuling(_transactionId, RECEIVER_WINS); + } else if (transaction.status == Status.WaitingReceiver) { + if (transaction.receiverFee != 0) { + uint256 receiverFee = transaction.receiverFee; + transaction.receiverFee = 0; + payable(transaction.receiver).call{value: receiverFee}(""); + } + _executeRuling(_transactionId, SENDER_WINS); + } + } + + /** + * @notice Allows a party to submit a reference to evidence. + * @param _profileId The TalentLayer ID of the user + * @param _transactionId The index of the transaction. + * @param _evidence A link to an evidence using its URI. + */ + function submitEvidence( + uint256 _profileId, + uint256 _transactionId, + string memory _evidence + ) public onlyOwnerOrDelegate(_profileId) { + require(bytes(_evidence).length == 46, "Invalid cid"); + Transaction storage transaction = transactions[_transactionId]; + + require(address(transaction.arbitrator) != address(0), "Arbitrator not set"); + + address party = talentLayerIdContract.ownerOf(_profileId); + require( + party == transaction.sender || party == transaction.receiver, + "The caller must be the sender or the receiver or their delegates" + ); + require(transaction.status < Status.Resolved, "Must not send evidence if the dispute is resolved"); + + emit Evidence(transaction.arbitrator, _transactionId, party, _evidence); + emit EvidenceSubmitted(_transactionId, _profileId, _evidence); + } + + /** + * @notice Appeals an appealable ruling, paying the appeal fee to the arbitrator. + * Note that no checks are required as the checks are done by the arbitrator. + * + * @param _transactionId Id of the transaction. + */ + function appeal(uint256 _transactionId) public payable { + Transaction storage transaction = transactions[_transactionId]; + + require(address(transaction.arbitrator) != address(0), "Arbitrator not set"); + + transaction.arbitrator.appeal{value: msg.value}(transaction.disputeId, transaction.arbitratorExtraData); + } + + // =========================== Platform functions ============================== + + /** + * @notice Allows a platform owner to claim its tokens & / or ETH balance. + * @param _platformId The ID of the platform claiming the balance. + * @param _tokenAddress The address of the Token contract (address(0) if balance in ETH). + * Emits a BalanceTransferred event + */ + function claim(uint256 _platformId, address _tokenAddress) external whenNotPaused { + address payable recipient; + + if (owner() == _msgSender()) { + require(_platformId == PROTOCOL_INDEX, "Access denied"); + recipient = protocolWallet; + } else { + talentLayerPlatformIdContract.isValid(_platformId); + recipient = payable(talentLayerPlatformIdContract.ownerOf(_platformId)); + } + + uint256 amount = platformIdToTokenToBalance[_platformId][_tokenAddress]; + require(amount > 0, "nothing to claim"); + platformIdToTokenToBalance[_platformId][_tokenAddress] = 0; + _safeTransferBalance(recipient, _tokenAddress, amount); + + emit FeesClaimed(_platformId, _tokenAddress, amount); + } + + // =========================== Arbitrator functions ============================== + + /** + * @notice Allows the arbitrator to give a ruling for a dispute. + * @param _disputeID The ID of the dispute in the Arbitrator contract. + * @param _ruling Ruling given by the arbitrator. Note that 0 is reserved for "Not able/wanting to make a decision". + */ + function rule(uint256 _disputeID, uint256 _ruling) public { + address sender = _msgSender(); + uint256 transactionId = disputeIDtoTransactionID[_disputeID]; + Transaction storage transaction = transactions[transactionId]; + + require(sender == address(transaction.arbitrator), "The caller must be the arbitrator"); + require(transaction.status == Status.DisputeCreated, "The dispute has already been resolved"); + + emit Ruling(Arbitrator(sender), _disputeID, _ruling); + + _executeRuling(transactionId, _ruling); + } + + // =========================== Internal functions ============================== + + /** + * @notice Creates a dispute, paying the arbitration fee to the arbitrator. Parties are refund if + * they overpaid for the arbitration fee. + * @param _transactionId Id of the transaction. + * @param _arbitrationCost Amount to pay the arbitrator. + */ + function _raiseDispute(uint256 _transactionId, uint256 _arbitrationCost) internal { + Transaction storage transaction = transactions[_transactionId]; + transaction.status = Status.DisputeCreated; + Arbitrator arbitrator = transaction.arbitrator; + + transaction.disputeId = arbitrator.createDispute{value: _arbitrationCost}( + AMOUNT_OF_CHOICES, + transaction.arbitratorExtraData + ); + disputeIDtoTransactionID[transaction.disputeId] = _transactionId; + emit Dispute(arbitrator, transaction.disputeId, _transactionId, _transactionId); + + // Refund sender if it overpaid. + if (transaction.senderFee > _arbitrationCost) { + uint256 extraFeeSender = transaction.senderFee - _arbitrationCost; + transaction.senderFee = _arbitrationCost; + payable(transaction.sender).call{value: extraFeeSender}(""); + emit ArbitrationFeePayment(_transactionId, ArbitrationFeePaymentType.Reimburse, Party.Sender, msg.value); + } + + // Refund receiver if it overpaid. + if (transaction.receiverFee > _arbitrationCost) { + uint256 extraFeeReceiver = transaction.receiverFee - _arbitrationCost; + transaction.receiverFee = _arbitrationCost; + payable(transaction.receiver).call{value: extraFeeReceiver}(""); + emit ArbitrationFeePayment(_transactionId, ArbitrationFeePaymentType.Reimburse, Party.Receiver, msg.value); + } + } + + /** + * @notice Executes a ruling of a dispute. Sends the funds and reimburses the arbitration fee to the winning party. + * @param _transactionId The index of the transaction. + * @param _ruling Ruling given by the arbitrator. + * 0: Refused to rule, split amount equally between sender and receiver. + * 1: Reimburse the sender + * 2: Pay the receiver + */ + function _executeRuling(uint256 _transactionId, uint256 _ruling) internal { + Transaction storage transaction = transactions[_transactionId]; + require(_ruling <= AMOUNT_OF_CHOICES, "Invalid ruling"); + + address payable sender = payable(transaction.sender); + address payable receiver = payable(transaction.receiver); + uint256 amount = transaction.amount; + uint256 senderFee = transaction.senderFee; + uint256 receiverFee = transaction.receiverFee; + + transaction.amount = 0; + transaction.senderFee = 0; + transaction.receiverFee = 0; + transaction.status = Status.Resolved; + + // Send the funds to the winner and reimburse the arbitration fee. + if (_ruling == SENDER_WINS) { + sender.call{value: senderFee}(""); + _reimburse(_transactionId, amount); + } else if (_ruling == RECEIVER_WINS) { + receiver.call{value: receiverFee}(""); + _release(_transactionId, amount); + } else { + // If no ruling is given split funds in half + uint256 splitFeeAmount = senderFee / 2; + uint256 splitTransactionAmount = amount / 2; + + _reimburse(_transactionId, splitTransactionAmount); + _release(_transactionId, splitTransactionAmount); + + sender.call{value: splitFeeAmount}(""); + receiver.call{value: splitFeeAmount}(""); + } + + emit RulingExecuted(_transactionId, _ruling); + } + + /** + * @notice Function that revert when `_msgSender()` is not authorized to upgrade the contract. Called by + * {upgradeTo} and {upgradeToAndCall}. + * @param newImplementation address of the new contract implementation + */ + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + + // =========================== Private functions ============================== + + /** + * @notice Emits the events related to the creation of a transaction. + * @param _senderId The TL ID of the sender + * @param _receiverId The TL ID of the receiver + * @param _transactionId The ID of the transavtion + * @param _metaEvidence The meta evidence of the transaction + */ + function _afterCreateTransaction( + uint256 _senderId, + uint256 _receiverId, + uint256 _transactionId, + string memory _metaEvidence + ) internal { + Transaction storage transaction = transactions[_transactionId]; + + emit TransactionCreated( + _transactionId, + _senderId, + _receiverId, + transaction.token, + transaction.amount, + transaction.serviceId, + transaction.proposalId, + protocolEscrowFeeRate, + transaction.originServiceFeeRate, + transaction.originValidatedProposalFeeRate, + transaction.arbitrator, + transaction.arbitratorExtraData, + transaction.arbitrationFeeTimeout + ); + emit MetaEvidence(_transactionId, _metaEvidence); + } + + /** + * @notice Used to release part of the escrow payment to the receiver. + * @dev The release of an amount will also trigger the release of the fees to the platform's balances & the protocol fees. + * @param _transactionId The transaction to release the escrow value for + * @param _amount The amount to release + */ + function _release(uint256 _transactionId, uint256 _amount) private { + _distributeFees(_transactionId, _amount); + + Transaction storage transaction = transactions[_transactionId]; + _safeTransferBalance(payable(transaction.receiver), transaction.token, _amount); + + _afterPayment(_transactionId, PaymentType.Release, _amount); + } + + /** + * @notice Used to reimburse part of the escrow payment to the sender. + * @dev Fees linked to the amount reimbursed will be automatically calculated and send back to the sender in the same transfer + * @param _transactionId The transaction + * @param _amount The amount to reimburse without fees + */ + function _reimburse(uint256 _transactionId, uint256 _amount) private { + Transaction storage transaction = transactions[_transactionId]; + uint256 totalReleaseAmount = _calculateTotalWithFees( + _amount, + transaction.originServiceFeeRate, + transaction.originValidatedProposalFeeRate + ); + _safeTransferBalance(payable(transaction.sender), transaction.token, totalReleaseAmount); + + _afterPayment(_transactionId, PaymentType.Reimburse, _amount); + } + + /** + * @notice Distribute fees to the platform's balances & the protocol after a fund release + * @param _transactionId The transaction linked to the payment + * @param _releaseAmount The amount released + */ + function _distributeFees(uint256 _transactionId, uint256 _releaseAmount) private { + Transaction storage transaction = transactions[_transactionId]; + ( + ITalentLayerService.Service memory service, + ITalentLayerService.Proposal memory proposal + ) = talentLayerServiceContract.getServiceAndProposal(transaction.serviceId, transaction.proposalId); + + uint256 originServiceCreationPlatformId = service.platformId; + uint256 originValidatedProposalPlatformId = proposal.platformId; + + uint256 protocolEscrowFeeRateAmount = (transaction.protocolEscrowFeeRate * _releaseAmount) / FEE_DIVIDER; + uint256 originServiceFeeRate = (transaction.originServiceFeeRate * _releaseAmount) / FEE_DIVIDER; + uint256 originValidatedProposalFeeRate = (transaction.originValidatedProposalFeeRate * _releaseAmount) / + FEE_DIVIDER; + + platformIdToTokenToBalance[PROTOCOL_INDEX][transaction.token] += protocolEscrowFeeRateAmount; + platformIdToTokenToBalance[originServiceCreationPlatformId][transaction.token] += originServiceFeeRate; + platformIdToTokenToBalance[originValidatedProposalPlatformId][ + transaction.token + ] += originValidatedProposalFeeRate; + + emit OriginServiceFeeRateReleased( + originServiceCreationPlatformId, + transaction.serviceId, + transaction.token, + originServiceFeeRate + ); + emit OriginValidatedProposalFeeRateReleased( + originValidatedProposalPlatformId, + transaction.serviceId, + transaction.token, + originServiceFeeRate + ); + } + + /** + * @notice Used to validate a realease or a reimburse payment + * @param _transactionId The transaction linked to the payment + * @param _paymentType The type of payment to validate + * @param _profileId The profileId of the msgSender + * @param _amount The amount to release + */ + function _validatePayment( + uint256 _transactionId, + PaymentType _paymentType, + uint256 _profileId, + uint256 _amount + ) private view { + Transaction storage _transaction = transactions[_transactionId]; + if (_paymentType == PaymentType.Release) { + require(_transaction.sender == talentLayerIdContract.ownerOf(_profileId), "Access denied"); + } else if (_paymentType == PaymentType.Reimburse) { + require(_transaction.receiver == talentLayerIdContract.ownerOf(_profileId), "Access denied"); + } + + require(_transaction.id < nextTransactionId.current(), "Invalid transaction id"); + require(_transaction.status == Status.NoDispute, "The transaction shouldn't be disputed"); + require(_transaction.amount >= _amount, "Insufficient funds"); + require(_amount >= FEE_DIVIDER || (_amount < FEE_DIVIDER && _amount == _transaction.amount), "Amount too low"); + } + + /** + * @notice Used to trigger events after a payment and to complete a service when the payment is full + * @param _transactionId The transaction linked to the payment + * @param _paymentType The type of the payment + * @param _releaseAmount The amount of the transaction + */ + function _afterPayment(uint256 _transactionId, PaymentType _paymentType, uint256 _releaseAmount) private { + Transaction storage transaction = transactions[_transactionId]; + emit Payment(transaction.id, _paymentType, transaction.token, _releaseAmount, transaction.serviceId); + + if (transaction.amount == 0) { + talentLayerServiceContract.afterFullPayment(transaction.serviceId, transaction.releasedAmount); + emit PaymentCompleted(transaction.serviceId); + } + } + + /** + * @notice Used to transfer the token or ETH balance from the escrow contract to a recipient's address. + * @param _recipient The address to transfer the balance to + * @param _tokenAddress The token address + * @param _amount The amount to transfer + */ + function _safeTransferBalance(address payable _recipient, address _tokenAddress, uint256 _amount) private { + if (address(0) == _tokenAddress) { + _recipient.call{value: _amount}(""); + } else { + IERC20(_tokenAddress).transfer(_recipient, _amount); + } + } + + /** + * @notice Utility function to calculate the total amount to be paid by the buyer to validate a proposal. + * @param _amount The core escrow amount + * @param _originServiceFeeRate the %fee (per ten thousands) asked by the platform for each service created on the platform + * @param _originValidatedProposalFeeRate the %fee (per ten thousands) asked by the platform for each validates service on the platform + * @return totalEscrowAmount The total amount to be paid by the buyer (including all fees + escrow) The amount to transfer + */ + function _calculateTotalWithFees( + uint256 _amount, + uint16 _originServiceFeeRate, + uint16 _originValidatedProposalFeeRate + ) private view returns (uint256 totalEscrowAmount) { + return + _amount + + (((_amount * protocolEscrowFeeRate) + + (_amount * _originServiceFeeRate) + + (_amount * _originValidatedProposalFeeRate)) / FEE_DIVIDER); + } + + // =========================== Overrides ============================== + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771RecipientUpgradeable) + returns (address) + { + return ERC2771RecipientUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771RecipientUpgradeable) + returns (bytes calldata) + { + return ERC2771RecipientUpgradeable._msgData(); + } +} diff --git a/contracts/archive/TalentLayerPlatformIdV1.sol b/contracts/archive/TalentLayerPlatformIdV1.sol new file mode 100644 index 00000000..bc851fdb --- /dev/null +++ b/contracts/archive/TalentLayerPlatformIdV1.sol @@ -0,0 +1,694 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {Base64Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/Base64Upgradeable.sol"; +import {CountersUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol"; +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {Arbitrator} from "../Arbitrator.sol"; + +/** + * @title Platform ID Contract + * @author TalentLayer Team | Website: https://talentlayer.org | Twitter: @talentlayer + */ +contract TalentLayerPlatformIDV1 is ERC721Upgradeable, AccessControlUpgradeable, UUPSUpgradeable { + using CountersUpgradeable for CountersUpgradeable.Counter; + + uint8 constant MIN_HANDLE_LENGTH = 5; + uint8 constant MAX_HANDLE_LENGTH = 31; + + // =========================== Enum ============================== + + /** + * @notice Enum for the mint status + */ + enum MintStatus { + ON_PAUSE, + ONLY_WHITELIST, + PUBLIC + } + + // =========================== Variables ============================== + + /** + * @notice TalentLayer Platform information struct + * @param id the TalentLayer Platform Id + * @param name the name of the platform + * @param dataUri the IPFS URI of the Platform metadata + * @param originServiceFeeRate the %fee (per ten thousands) asked by the platform for each service created on the platform + * @param originValidatedProposalFeeRate the %fee (per ten thousands) asked by the platform for each validates service on the platform + * @param servicePostingFee the fee (flat) asked by the platform to post a service on the platform + * @param proposalPostingFee the fee (flat) asked by the platform to post a proposal on the platform + * @param arbitrator address of the arbitrator used by the platform + * @param arbitratorExtraData extra information for the arbitrator + * @param arbitrationFeeTimeout timeout for parties to pay the arbitration fee + * @param signer address used to sign operations which need platform authorization + */ + struct Platform { + uint256 id; + string name; + string dataUri; + uint16 originServiceFeeRate; + uint16 originValidatedProposalFeeRate; + uint256 servicePostingFee; + uint256 proposalPostingFee; + Arbitrator arbitrator; + bytes arbitratorExtraData; + uint256 arbitrationFeeTimeout; + address signer; + } + + /** + * @notice Taken Platform name + */ + mapping(string => bool) public takenNames; + + /** + * @notice Platform ID to Platform struct + */ + mapping(uint256 => Platform) public platforms; + + /** + * @notice Addresses which are available as arbitrators + */ + mapping(address => bool) public validArbitrators; + + /** + * @notice Addresses which are allowed to mint a Platform ID + */ + mapping(address => bool) public whitelist; + + /** + * @notice Whether arbitrators are internal (are part of TalentLayer) or not + * Internal arbitrators will have the extra data set to the platform ID + */ + mapping(address => bool) public internalArbitrators; + + /** + * @notice Address to PlatformId + */ + mapping(address => uint256) public ids; + + /** + * @notice Price to mint a platform id (in wei, upgradable) + */ + uint256 public mintFee; + + /** + * @notice Role granting Minting permission + */ + bytes32 public constant MINT_ROLE = keccak256("MINT_ROLE"); + + /** + * @notice Minimum timeout to pay arbitration fee + */ + uint256 public minArbitrationFeeTimeout; + + /** + * @notice Platform Id counter + */ + CountersUpgradeable.Counter private nextPlatformId; + + /** + * @notice The minting status + */ + MintStatus public mintStatus; + + // =========================== Errors ============================== + + /** + * @notice error thrown when input handle is 0 or more than 31 characters long. + */ + error HandleLengthInvalid(); + + /** + * @notice error thrown when input handle contains restricted characters. + */ + error HandleContainsInvalidCharacters(); + + /** + * @notice error thrown when input handle has an invalid first character. + */ + error HandleFirstCharInvalid(); + + // =========================== Initializers ============================== + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize() public initializer { + __ERC721_init("TalentLayerPlatformID", "TLPID"); + __AccessControl_init(); + __UUPSUpgradeable_init(); + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + _setupRole(MINT_ROLE, msg.sender); + mintFee = 0; + validArbitrators[address(0)] = true; // The zero address means no arbitrator. + updateMinArbitrationFeeTimeout(10 days); + // Increment counter to start platform ids at index 1 + nextPlatformId.increment(); + mintStatus = MintStatus.ONLY_WHITELIST; + } + + // =========================== View functions ============================== + + /** + * @notice Check whether the TalentLayer Platform Id is valid. + * @param _platformId The Platform Id. + */ + function isValid(uint256 _platformId) public view { + require(_platformId > 0 && _platformId < nextPlatformId.current(), "Invalid platform ID"); + } + + /** + * @notice Allows retrieval of a Platform fee + * @param _platformId The Platform Id + * @return The Platform fee + */ + function getOriginServiceFeeRate(uint256 _platformId) external view returns (uint16) { + isValid(_platformId); + return platforms[_platformId].originServiceFeeRate; + } + + /** + * @notice Allows retrieval of a Platform fee + * @param _platformId The Platform Id + * @return The Platform fee + */ + function getOriginValidatedProposalFeeRate(uint256 _platformId) external view returns (uint16) { + isValid(_platformId); + return platforms[_platformId].originValidatedProposalFeeRate; + } + + /** + * @notice Allows retrieval of a service posting fee + * @param _platformId The Platform Id + * @return The Service posting fee + */ + function getServicePostingFee(uint256 _platformId) external view returns (uint256) { + isValid(_platformId); + return platforms[_platformId].servicePostingFee; + } + + /** + * @notice Allows retrieval of a proposal posting fee + * @param _platformId The Platform Id + * @return The Proposal posting fee + */ + function getProposalPostingFee(uint256 _platformId) external view returns (uint256) { + isValid(_platformId); + return platforms[_platformId].proposalPostingFee; + } + + /** + * @notice Allows retrieval of the signer of a platform + * @param _platformId The Platform Id + * @return The signer of the platform + */ + function getSigner(uint256 _platformId) external view returns (address) { + isValid(_platformId); + return platforms[_platformId].signer; + } + + /** + * @notice Allows retrieval of a Platform arbitrator + * @param _platformId The Platform Id + * @return Arbitrator The Platform arbitrator + */ + function getPlatform(uint256 _platformId) external view returns (Platform memory) { + isValid(_platformId); + return platforms[_platformId]; + } + + /** + * @dev Returns the total number of tokens in existence. + */ + function totalSupply() public view returns (uint256) { + return nextPlatformId.current() - 1; + } + + // =========================== User functions ============================== + + /** + * @notice Allows a platform to mint a new Platform Id. + * @param _platformName Platform name + */ + function mint(string calldata _platformName) public payable canMint(_platformName, msg.sender) returns (uint256) { + _mint(msg.sender, nextPlatformId.current()); + return _afterMint(_platformName, msg.sender); + } + + /** + * @notice Allows a user to mint a new Platform Id and assign it to an eth address. + * @dev You need to have MINT_ROLE to use this function + * @param _platformName Platform name + * @param _platformAddress Eth Address to assign the Platform Id to + */ + function mintForAddress( + string calldata _platformName, + address _platformAddress + ) public payable canMint(_platformName, _platformAddress) onlyRole(MINT_ROLE) returns (uint256) { + _mint(_platformAddress, nextPlatformId.current()); + return _afterMint(_platformName, _platformAddress); + } + + /** + * @notice Update platform URI data. + * @dev we are trusting the platform to provide the valid IPFS URI + * @param _platformId The Platform Id + * @param _newCid New IPFS URI + */ + function updateProfileData(uint256 _platformId, string memory _newCid) public onlyPlatformOwner(_platformId) { + require(bytes(_newCid).length == 46, "Invalid cid"); + + platforms[_platformId].dataUri = _newCid; + + emit CidUpdated(_platformId, _newCid); + } + + /** + * @notice Allows a platform to update his fee + * @param _platformId The Platform Id + * @param _originServiceFeeRate Platform fee to update + */ + function updateOriginServiceFeeRate( + uint256 _platformId, + uint16 _originServiceFeeRate + ) public onlyPlatformOwner(_platformId) { + platforms[_platformId].originServiceFeeRate = _originServiceFeeRate; + emit OriginServiceFeeRateUpdated(_platformId, _originServiceFeeRate); + } + + /** + * @notice Allows a platform to update his fee + * @param _platformId The Platform Id + * @param _originValidatedProposalFeeRate Platform fee to update + */ + function updateOriginValidatedProposalFeeRate( + uint256 _platformId, + uint16 _originValidatedProposalFeeRate + ) public onlyPlatformOwner(_platformId) { + platforms[_platformId].originValidatedProposalFeeRate = _originValidatedProposalFeeRate; + emit OriginValidatedProposalFeeRateUpdated(_platformId, _originValidatedProposalFeeRate); + } + + /** + * @notice Allows a platform to update his arbitrator + * @param _platformId The Platform Id + * @param _arbitrator the arbitrator + * @param _extraData the extra data for arbitrator (this is only used for external arbitrators, for internal arbitrators it should be empty) + */ + function updateArbitrator( + uint256 _platformId, + Arbitrator _arbitrator, + bytes memory _extraData + ) public onlyPlatformOwner(_platformId) { + require(validArbitrators[address(_arbitrator)], "The address must be of a valid arbitrator"); + + platforms[_platformId].arbitrator = _arbitrator; + + if (internalArbitrators[address(_arbitrator)]) { + platforms[_platformId].arbitratorExtraData = abi.encodePacked(_platformId); + } else { + platforms[_platformId].arbitratorExtraData = _extraData; + } + + emit ArbitratorUpdated(_platformId, _arbitrator, platforms[_platformId].arbitratorExtraData); + } + + /** + * @notice Allows a platform to update the timeout for paying the arbitration fee + * @param _platformId The Platform Id + * @param _arbitrationFeeTimeout The new timeout + */ + function updateArbitrationFeeTimeout( + uint256 _platformId, + uint256 _arbitrationFeeTimeout + ) public onlyPlatformOwner(_platformId) { + require( + _arbitrationFeeTimeout >= minArbitrationFeeTimeout, + "The timeout must be greater than the minimum timeout" + ); + + platforms[_platformId].arbitrationFeeTimeout = _arbitrationFeeTimeout; + emit ArbitrationFeeTimeoutUpdated(_platformId, _arbitrationFeeTimeout); + } + + /** + * @notice Allows a platform to update the service posting fee for the platform + * @param _platformId The platform Id of the platform + * @param _servicePostingFee The new fee + */ + function updateServicePostingFee( + uint256 _platformId, + uint256 _servicePostingFee + ) public onlyPlatformOwner(_platformId) { + platforms[_platformId].servicePostingFee = _servicePostingFee; + emit ServicePostingFeeUpdated(_platformId, _servicePostingFee); + } + + /** + * @notice Allows a platform to update the proposal posting fee for the platform + * @param _platformId The platform Id of the platform + * @param _proposalPostingFee The new fee + */ + function updateProposalPostingFee( + uint256 _platformId, + uint256 _proposalPostingFee + ) public onlyPlatformOwner(_platformId) { + platforms[_platformId].proposalPostingFee = _proposalPostingFee; + emit ProposalPostingFeeUpdated(_platformId, _proposalPostingFee); + } + + /** + * @notice Allows a platform to update its signer address + * @param _platformId The platform Id of the platform + * @param _signer The new signer address + */ + function updateSigner(uint256 _platformId, address _signer) public onlyPlatformOwner(_platformId) { + platforms[_platformId].signer = _signer; + emit SignerUpdated(_platformId, _signer); + } + + // =========================== Owner functions ============================== + + /** + * @notice whitelist a user. + * @param _user Address of the user to whitelist + */ + function whitelistUser(address _user) public onlyRole(DEFAULT_ADMIN_ROLE) { + whitelist[_user] = true; + emit UserWhitelisted(_user); + } + + /** + * @notice Updates the mint status. + * @param _mintStatus The new mint status + */ + function updateMintStatus(MintStatus _mintStatus) public onlyRole(DEFAULT_ADMIN_ROLE) { + mintStatus = _mintStatus; + emit MintStatusUpdated(_mintStatus); + } + + /** + * Updates the mint fee. + * @param _mintFee The new mint fee + */ + function updateMintFee(uint256 _mintFee) public onlyRole(DEFAULT_ADMIN_ROLE) { + mintFee = _mintFee; + emit MintFeeUpdated(_mintFee); + } + + /** + * Withdraws the contract balance to the admin. + */ + function withdraw() public onlyRole(DEFAULT_ADMIN_ROLE) { + (bool sent, ) = payable(msg.sender).call{value: address(this).balance}(""); + require(sent, "Failed to withdraw Ether"); + } + + /** + * @notice Adds a new available arbitrator. + * @param _arbitrator address of the arbitrator + * @param _isInternal whether the arbitrator is internal (is part of TalentLayer) or not + * @dev You need to have DEFAULT_ADMIN_ROLE to use this function + */ + function addArbitrator(address _arbitrator, bool _isInternal) public onlyRole(DEFAULT_ADMIN_ROLE) { + validArbitrators[address(_arbitrator)] = true; + internalArbitrators[address(_arbitrator)] = _isInternal; + } + + /** + * @notice Removes an available arbitrator. + * @param _arbitrator address of the arbitrator + * @dev You need to have DEFAULT_ADMIN_ROLE to use this function + */ + function removeArbitrator(address _arbitrator) public onlyRole(DEFAULT_ADMIN_ROLE) { + validArbitrators[address(_arbitrator)] = false; + internalArbitrators[address(_arbitrator)] = false; + } + + /** + * @notice Updates the minimum timeout for paying the arbitration fee. + * @param _minArbitrationFeeTimeout The new minimum timeout + * @dev You need to have DEFAULT_ADMIN_ROLE to use this function + */ + function updateMinArbitrationFeeTimeout(uint256 _minArbitrationFeeTimeout) public onlyRole(DEFAULT_ADMIN_ROLE) { + minArbitrationFeeTimeout = _minArbitrationFeeTimeout; + emit MinArbitrationFeeTimeoutUpdated(_minArbitrationFeeTimeout); + } + + // =========================== Private functions ============================== + + /** + * @notice Update Platform name mapping and emit event after mint. + * @param _platformName Name of the platform. + * @param _platformAddress Address of the platform. + * @dev Increments the nextTokenId counter. + */ + function _afterMint(string memory _platformName, address _platformAddress) private returns (uint256) { + uint256 platformId = nextPlatformId.current(); + nextPlatformId.increment(); + Platform storage platform = platforms[platformId]; + platform.name = _platformName; + platform.id = platformId; + platform.arbitrationFeeTimeout = minArbitrationFeeTimeout; + platform.signer = address(0); + takenNames[_platformName] = true; + ids[_platformAddress] = platformId; + + emit Mint(_platformAddress, platformId, _platformName, mintFee, minArbitrationFeeTimeout); + + return platformId; + } + + /** + * @notice Validate characters used in the handle, only alphanumeric, only lowercase characters, - and _ are allowed but as first one + * @param handle Handle to validate + */ + function _validateHandle(string calldata handle) private pure { + bytes memory byteHandle = bytes(handle); + uint256 byteHandleLength = byteHandle.length; + if (byteHandleLength < MIN_HANDLE_LENGTH || byteHandleLength > MAX_HANDLE_LENGTH) revert HandleLengthInvalid(); + + bytes1 firstByte = bytes(handle)[0]; + if (firstByte == "-" || firstByte == "_") revert HandleFirstCharInvalid(); + + for (uint256 i = 0; i < byteHandleLength; ) { + if ( + (byteHandle[i] < "0" || byteHandle[i] > "z" || (byteHandle[i] > "9" && byteHandle[i] < "a")) && + byteHandle[i] != "-" && + byteHandle[i] != "_" + ) revert HandleContainsInvalidCharacters(); + ++i; + } + } + + // =========================== Overrides ============================== + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC721Upgradeable, AccessControlUpgradeable) returns (bool) { + return + ERC721Upgradeable.supportsInterface(interfaceId) || AccessControlUpgradeable.supportsInterface(interfaceId); + } + + /** + * @dev Override to prevent token transfer. + */ + function _transfer(address, address, uint256) internal virtual override(ERC721Upgradeable) { + revert("Token transfer is not allowed"); + } + + /** + * @notice Implementation of the {IERC721Metadata-tokenURI} function. + * @param tokenId The ID of the token + */ + function tokenURI(uint256 tokenId) public view virtual override(ERC721Upgradeable) returns (string memory) { + return _buildTokenURI(tokenId); + } + + /** + * @notice Builds the token URI + * @param id The ID of the token + */ + function _buildTokenURI(uint256 id) internal view returns (string memory) { + string memory platformName = string.concat(platforms[id].name, ".tlp"); + string memory fontSizeStr = bytes(platforms[id].name).length <= 20 ? "60" : "40"; + + bytes memory image = abi.encodePacked( + "data:image/svg+xml;base64,", + Base64Upgradeable.encode( + bytes( + abi.encodePacked( + '', + platformName, + "" + ) + ) + ) + ); + return + string( + abi.encodePacked( + "data:application/json;base64,", + Base64Upgradeable.encode( + bytes( + abi.encodePacked( + '{"name":"', + platformName, + '", "image":"', + image, + unicode'", "description": "TalentLayer Platform ID"}' + ) + ) + ) + ) + ); + } + + /** + * @notice Function that revert when `msg.sender` is not authorized to upgrade the contract. Called by + * {upgradeTo} and {upgradeToAndCall}. + * @param newImplementation address of the new contract implementation + */ + function _authorizeUpgrade( + address newImplementation + ) internal override(UUPSUpgradeable) onlyRole(DEFAULT_ADMIN_ROLE) {} + + // =========================== Modifiers ============================== + + /** + * @notice Check if Platform is able to mint a new Platform ID. + * @param _platformName name for the platform + * @param _platformAddress address of the platform associated with the ID + */ + modifier canMint(string calldata _platformName, address _platformAddress) { + require(mintStatus == MintStatus.ONLY_WHITELIST || mintStatus == MintStatus.PUBLIC, "Mint status is not valid"); + if (mintStatus == MintStatus.ONLY_WHITELIST) { + require(whitelist[msg.sender], "You are not whitelisted"); + } + require(msg.value == mintFee, "Incorrect amount of ETH for mint fee"); + require(balanceOf(_platformAddress) == 0, "Platform already has a Platform ID"); + require(!takenNames[_platformName], "Name already taken"); + + _validateHandle(_platformName); + _; + } + + /** + * @notice Check if msg sender is the owner of a platform + * @param _platformId the ID of the platform + */ + modifier onlyPlatformOwner(uint256 _platformId) { + require(ownerOf(_platformId) == msg.sender, "Not the owner"); + _; + } + + // =========================== Events ============================== + + /** + * @notice Emit when new Platform ID is minted. + * @param platformOwnerAddress Address of the owner of the PlatformID + * @param platformId The Platform ID + * @param platformName Name of the platform + * @param fee Fee paid to mint the Platform ID + * @param arbitrationFeeTimeout Timeout to pay arbitration fee + */ + event Mint( + address indexed platformOwnerAddress, + uint256 platformId, + string platformName, + uint256 fee, + uint256 arbitrationFeeTimeout + ); + + /** + * @notice Emit when Cid is updated for a platform. + * @param platformId The Platform ID + * @param newCid New URI + */ + event CidUpdated(uint256 indexed platformId, string newCid); + + /** + * @notice Emit when mint fee is updated + * @param mintFee The new mint fee + */ + event MintFeeUpdated(uint256 mintFee); + + /** + * @notice Emit when the fee is updated for a platform + * @param platformId The Platform Id + * @param originServiceFeeRate The new fee + */ + event OriginServiceFeeRateUpdated(uint256 platformId, uint16 originServiceFeeRate); + + /** + * @notice Emit when the fee is updated for a platform + * @param platformId The Platform Id + * @param originValidatedProposalFeeRate The new fee + */ + event OriginValidatedProposalFeeRateUpdated(uint256 platformId, uint16 originValidatedProposalFeeRate); + + /** + * @notice Emit after the arbitrator is updated for a platform + * @param platformId The Platform Id + * @param arbitrator The address of the new arbitrator + * @param extraData The new extra data for the arbitrator + */ + event ArbitratorUpdated(uint256 platformId, Arbitrator arbitrator, bytes extraData); + + /** + * @notice Emit after the arbitration fee timeout is updated for a platform + * @param platformId The Platform Id + * @param arbitrationFeeTimeout The new arbitration fee timeout + */ + event ArbitrationFeeTimeoutUpdated(uint256 platformId, uint256 arbitrationFeeTimeout); + + /** + * @notice Emit after the minimum arbitration fee timeout is updated + * @param minArbitrationFeeTimeout The new arbitration fee timeout + */ + event MinArbitrationFeeTimeoutUpdated(uint256 minArbitrationFeeTimeout); + + /** + * @notice Emit when the service posting fee is updated for a platform + * @param platformId The Platform Id + * @param servicePostingFee The new fee + */ + event ServicePostingFeeUpdated(uint256 platformId, uint256 servicePostingFee); + + /** + * @notice Emit when the proposal posting fee is updated for a platform + * @param platformId The Platform Id + * @param proposalPostingFee The new fee + */ + event ProposalPostingFeeUpdated(uint256 platformId, uint256 proposalPostingFee); + + /** + * @notice Emit when the signer address is updated for a platform + * @param platformId The Platform Id + * @param signer The new signer address + */ + event SignerUpdated(uint256 platformId, address signer); + + /** + * @notice Emit when the minting status is updated + * @param mintStatus The new mint status + */ + event MintStatusUpdated(MintStatus mintStatus); + + /** + * @notice Emit when a platform is whitelisted + * @param user The new address whitelited + */ + event UserWhitelisted(address indexed user); +} diff --git a/contracts/archive/TalentLayerReviewV1.sol b/contracts/archive/TalentLayerReviewV1.sol new file mode 100644 index 00000000..50554c96 --- /dev/null +++ b/contracts/archive/TalentLayerReviewV1.sol @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import {ERC2771RecipientUpgradeable} from "../libs/ERC2771RecipientUpgradeable.sol"; +import {ITalentLayerID} from "../interfaces/ITalentLayerID.sol"; +import {ITalentLayerService} from "../interfaces/ITalentLayerService.sol"; +import {ITalentLayerPlatformID} from "../interfaces/ITalentLayerPlatformID.sol"; + +import {Base64Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/Base64Upgradeable.sol"; +import {AddressUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {StringsUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import {CountersUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol"; +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +/** + * @title TalentLayer Review Contract + * @author TalentLayer Team | Website: https://talentlayer.org | Twitter: @talentlayer + */ +contract TalentLayerReviewV1 is ERC2771RecipientUpgradeable, ERC721Upgradeable, UUPSUpgradeable { + using AddressUpgradeable for address; + using StringsUpgradeable for uint256; + using CountersUpgradeable for CountersUpgradeable.Counter; + + /** + * @notice Review information struct + * @param id the id of the review + * @param ownerId the talentLayerId of the user who received the review + * @param dataUri the IPFS URI of the review metadata + * @param serviceId the id of the service of the review + * @param rating the rating of the review + */ + struct Review { + uint256 id; + uint256 ownerId; + string dataUri; + uint256 serviceId; + uint256 rating; + } + + /** + * @notice Review id counter + */ + CountersUpgradeable.Counter nextReviewId; + + /** + * @notice Review id to review + */ + mapping(uint256 => Review) public reviews; + + /** + * @notice Mapping to record whether the buyer has been reviewed for a service + */ + mapping(uint256 => bool) public hasBuyerBeenReviewed; + + /** + * @notice Mapping to record whether the seller has been reviewed for a service + */ + mapping(uint256 => bool) public hasSellerBeenReviewed; + + /** + * @notice TalentLayer contract instance + */ + ITalentLayerID private tlId; + + /** + * @notice TalentLayerService + */ + ITalentLayerService private talentLayerService; + + // =========================== Initializers ============================== + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address _talentLayerIdAddress, address _talentLayerServiceAddress) public initializer { + __ERC721_init("TalentLayerReview", "TLR"); + __UUPSUpgradeable_init(); + __Ownable_init(); + tlId = ITalentLayerID(_talentLayerIdAddress); + talentLayerService = ITalentLayerService(_talentLayerServiceAddress); + // Increment counter to start review ids at index 1 + nextReviewId.increment(); + } + + // =========================== View functions ============================== + + /** + * @notice Returns the review information + * @param _reviewId The id of the review + */ + function getReview(uint256 _reviewId) public view returns (Review memory) { + require(_reviewId < nextReviewId.current(), "Invalid review ID"); + return reviews[_reviewId]; + } + + /** + * @dev Returns the total number of tokens in existence. + */ + function totalSupply() public view returns (uint256) { + return nextReviewId.current() - 1; + } + + // =========================== User functions ============================== + + /** + * @notice Called to mint a review token for a completed service + * @dev Only one review can be minted per user + * @param _profileId The TalentLayer ID of the user + * @param _serviceId Service ID + * @param _reviewUri The IPFS URI of the review + * @param _rating The review rate + */ + function mint( + uint256 _profileId, + uint256 _serviceId, + string calldata _reviewUri, + uint256 _rating + ) public onlyOwnerOrDelegate(_profileId) returns (uint256) { + ITalentLayerService.Service memory service = talentLayerService.getService(_serviceId); + + require( + _profileId == service.ownerId || _profileId == service.acceptedProposalId, + "Not an actor of this service" + ); + require(service.status == ITalentLayerService.Status.Finished, "Service not finished yet"); + require(_rating <= 5, "Invalid rating"); + + uint256 toId; + if (_profileId == service.ownerId) { + toId = service.acceptedProposalId; + require(!hasSellerBeenReviewed[_serviceId], "Already minted"); + hasSellerBeenReviewed[_serviceId] = true; + } else { + toId = service.ownerId; + require(!hasBuyerBeenReviewed[_serviceId], "Already minted"); + hasBuyerBeenReviewed[_serviceId] = true; + } + + address sender = tlId.ownerOf(toId); + _safeMint(sender, nextReviewId.current()); + return _afterMint(_serviceId, toId, _rating, _reviewUri); + } + + // =========================== Private functions =========================== + + /** + * @dev After the mint of a review + * @param _serviceId The ID of the service linked to the review + * @param _to The address of the recipient + * @param _rating The review rate + * @param _reviewUri The IPFS URI of the review + */ + function _afterMint( + uint256 _serviceId, + uint256 _to, + uint256 _rating, + string calldata _reviewUri + ) private returns (uint256) { + uint256 reviewId = nextReviewId.current(); + nextReviewId.increment(); + + reviews[reviewId] = Review({ + id: reviewId, + ownerId: _to, + dataUri: _reviewUri, + serviceId: _serviceId, + rating: _rating + }); + + emit Mint(_serviceId, _to, reviewId, _rating, _reviewUri); + return reviewId; + } + + // =========================== Internal functions ========================== + + /** + * @notice Function that revert when `_msgSender()` is not authorized to upgrade the contract. Called by + * {upgradeTo} and {upgradeToAndCall}. + * @param newImplementation address of the new contract implementation + */ + function _authorizeUpgrade(address newImplementation) internal override(UUPSUpgradeable) onlyOwner {} + + // =========================== Overrides =================================== + + /** + * @dev Override to prevent token transfer. + */ + function _transfer(address, address, uint256) internal virtual override(ERC721Upgradeable) { + revert("Token transfer is not allowed"); + } + + /** + * @dev Blocks the burn function + * @param _tokenId The ID of the token + */ + function _burn(uint256 _tokenId) internal virtual override(ERC721Upgradeable) {} + + /** + * @notice Implementation of the {IERC721Metadata-tokenURI} function. + */ + function tokenURI(uint256) public view virtual override(ERC721Upgradeable) returns (string memory) { + return _buildTokenURI(); + } + + /** + * @notice Builds the token URI + */ + function _buildTokenURI() internal pure returns (string memory) { + bytes memory image = abi.encodePacked( + "data:image/svg+xml;base64,", + Base64Upgradeable.encode( + bytes( + 'review' + ) + ) + ); + return + string( + abi.encodePacked( + "data:application/json;base64,", + Base64Upgradeable.encode( + bytes(abi.encodePacked('{"name":"TalentLayer Review"', ', "image":"', image, unicode'"}')) + ) + ) + ); + } + + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771RecipientUpgradeable) + returns (address) + { + return ERC2771RecipientUpgradeable._msgSender(); + } + + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771RecipientUpgradeable) + returns (bytes calldata) + { + return ERC2771RecipientUpgradeable._msgData(); + } + + // =========================== Modifiers ============================== + + /** + * @notice Check if the given address is either the owner of the delegate of the given user + * @param _profileId The TalentLayer ID of the user + */ + modifier onlyOwnerOrDelegate(uint256 _profileId) { + require(tlId.isOwnerOrDelegate(_profileId, _msgSender()), "Not owner or delegate"); + _; + } + + // =========================== Events ====================================== + + /** + * @dev Emitted after a review token is minted + * @param serviceId The ID of the service + * @param toId The TalentLayer Id of the recipient + * @param tokenId The ID of the review token + * @param rating The rating of the review + * @param reviewUri The IPFS URI of the review metadata + */ + event Mint( + uint256 indexed serviceId, + uint256 indexed toId, + uint256 indexed tokenId, + uint256 rating, + string reviewUri + ); +} diff --git a/contracts/interfaces/ITalentLayerPlatformID.sol b/contracts/interfaces/ITalentLayerPlatformID.sol index 03ed73bb..8423fe40 100644 --- a/contracts/interfaces/ITalentLayerPlatformID.sol +++ b/contracts/interfaces/ITalentLayerPlatformID.sol @@ -76,4 +76,8 @@ interface ITalentLayerPlatformID is IERC721Upgradeable { event Mint(address indexed _platformOwnerAddress, uint256 _tokenId, string _platformName); event CidUpdated(uint256 indexed _tokenId, string _newCid); + + event ArbitratorAdded(address arbitrator, bool isInternal); + + event ArbitratorRemoved(address arbitrator); } diff --git a/hardhat.config.ts b/hardhat.config.ts index efb02587..bedaa11c 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -60,7 +60,7 @@ function getChainConfig(chain: Network): NetworkUserConfig { jsonRpcUrl = 'https://polygon-rpc.com/' break case Network.MUMBAI: - jsonRpcUrl = process.env.MUMBAI_RPC || 'https://matic-mumbai.chainstacklabs.com' + jsonRpcUrl = process.env.MUMBAI_RPC || 'https://polygon-mumbai-bor.publicnode.com' break default: jsonRpcUrl = 'https://mainnet.infura.io/v3/' + infuraApiKey @@ -74,6 +74,7 @@ function getChainConfig(chain: Network): NetworkUserConfig { }, chainId: chain, url: jsonRpcUrl, + gasMultiplier: 1.5, } } diff --git a/scripts/playground/0-mint-platform-ID.ts b/scripts/playground/0-mint-platform-ID.ts index 641d503f..053c20ef 100644 --- a/scripts/playground/0-mint-platform-ID.ts +++ b/scripts/playground/0-mint-platform-ID.ts @@ -2,7 +2,6 @@ import { ethers } from 'hardhat' import { DeploymentProperty, getDeploymentProperty } from '../../.deployment/deploymentManager' import hre = require('hardhat') import { cid, cid2 } from './constants' -import { expect } from 'chai' /* in this script we will mint a new platform ID for HireVibes diff --git a/test/batch/disputeResolution.ts b/test/batch/disputeResolution.ts index bb96e865..62a8e674 100644 --- a/test/batch/disputeResolution.ts +++ b/test/batch/disputeResolution.ts @@ -73,7 +73,13 @@ async function deployAndSetup( await talentLayerPlatformID.connect(deployer).mintForAddress(platformName, carol.address) // Add arbitrator to platform available arbitrators - await talentLayerPlatformID.connect(deployer).addArbitrator(talentLayerArbitrator.address, true) + expect( + await talentLayerPlatformID + .connect(deployer) + .addArbitrator(talentLayerArbitrator.address, true), + ) + .to.emit(talentLayerPlatformID, 'ArbitratorAdded') + .withArgs(talentLayerArbitrator.address, true) // Update platform arbitrator, and fee timeout await talentLayerPlatformID @@ -559,6 +565,7 @@ describe('Dispute Resolution, standard flow', function () { ethers.constants.AddressZero, currentTransactionAmount, serviceId, + proposalId, ) }) @@ -834,6 +841,7 @@ describe('Dispute Resolution, arbitrator abstaining from giving a ruling', funct ethers.constants.AddressZero, halfTransactionAmount, serviceId, + proposalId, ) await expect(tx) @@ -844,6 +852,7 @@ describe('Dispute Resolution, arbitrator abstaining from giving a ruling', funct ethers.constants.AddressZero, halfTransactionAmount, serviceId, + proposalId, ) }) }) @@ -933,6 +942,7 @@ describe('Dispute Resolution, receiver winning', function () { ethers.constants.AddressZero, transactionAmount, serviceId, + proposalId, ) }) }) diff --git a/test/batch/fullWorkflow.ts b/test/batch/fullWorkflow.ts index 5469708c..87921140 100644 --- a/test/batch/fullWorkflow.ts +++ b/test/batch/fullWorkflow.ts @@ -22,6 +22,7 @@ import { expiredProposalDate, proposalExpirationDate, ServiceStatus, + PaymentType, } from '../utils/constant' import { getSignatureForProposal, getSignatureForService } from '../utils/signature' @@ -377,9 +378,13 @@ describe('TalentLayer protocol global testing', function () { }) it('The deployer can add a new available arbitrator', async function () { - await talentLayerPlatformID - .connect(deployer) - .addArbitrator(talentLayerArbitrator.address, true) + expect( + await talentLayerPlatformID + .connect(deployer) + .addArbitrator(talentLayerArbitrator.address, true), + ) + .to.emit(talentLayerPlatformID, 'ArbitratorAdded') + .withArgs(talentLayerArbitrator.address, true) const isValid = await talentLayerPlatformID.validArbitrators(talentLayerArbitrator.address) expect(isValid).to.be.true @@ -441,7 +446,13 @@ describe('TalentLayer protocol global testing', function () { }) it('The deployer can remove an available arbitrator', async function () { - await talentLayerPlatformID.connect(deployer).removeArbitrator(talentLayerArbitrator.address) + expect( + await talentLayerPlatformID + .connect(deployer) + .removeArbitrator(talentLayerArbitrator.address), + ) + .to.emit(talentLayerPlatformID, 'ArbitratorRemoved') + .withArgs(talentLayerArbitrator.address) const isValid = await talentLayerPlatformID.validArbitrators(talentLayerArbitrator.address) expect(isValid).to.be.false @@ -1395,6 +1406,17 @@ describe('TalentLayer protocol global testing', function () { [-releasedAmount, 0, releasedAmount], ) + await expect(tx) + .to.emit(talentLayerEscrow, 'Payment') + .withArgs( + transactionId, + PaymentType.Release, + token.address, + releasedAmount, + serviceId, + transactionDetailsBefore.proposalId, + ) + // Check transaction data has been updated correctly const transactionDetailsAfter = await talentLayerEscrow .connect(alice) @@ -1525,7 +1547,7 @@ describe('TalentLayer protocol global testing', function () { await token.connect(alice).approve(talentLayerEscrow.address, totalAmount) - // we need to retreive the Bob proposal dataUri + // we need to retrieve the Bob proposal dataUri const proposal = await talentLayerService.proposals(serviceId, bobTlId) await expect( @@ -1567,6 +1589,16 @@ describe('TalentLayer protocol global testing', function () { [talentLayerEscrow.address, alice, bob], [-totalAmount / 4, totalAmount / 4, 0], ) + await expect(transaction) + .to.emit(talentLayerEscrow, 'Payment') + .withArgs( + transactionId, + PaymentType.Reimburse, + token.address, + reimburseAmount, + serviceId, + transactionDetailsBefore.proposalId, + ) await expect(transaction).to.emit(talentLayerEscrow, 'PaymentCompleted').withArgs(serviceId) // Check transaction data has been updated correctly @@ -1927,7 +1959,13 @@ describe('TalentLayer protocol global testing', function () { }) it('Alice can review Bob for the service they had', async function () { - await talentLayerReview.connect(alice).mint(aliceTlId, finishedServiceId, cid, 4) + const tx = await talentLayerReview.connect(alice).mint(aliceTlId, finishedServiceId, cid, 4) + + const reviewId = 1 + const acceptedProposalId = 2 + await expect(tx) + .to.emit(talentLayerReview, 'Mint') + .withArgs(finishedServiceId, bobTlId, reviewId, 4, cid, acceptedProposalId) const owner = await talentLayerReview.ownerOf(bobReviewId) expect(owner).to.be.equal(bob.address)