From 2b8a6622b27397186fa17c5c791a9125e331a7fb Mon Sep 17 00:00:00 2001 From: "Quentin D.C" Date: Tue, 9 Apr 2024 07:50:26 +0100 Subject: [PATCH 1/5] Removed .only --- test/batch/talentLayerIdUtils.ts | 2 +- test/manual/audit.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/batch/talentLayerIdUtils.ts b/test/batch/talentLayerIdUtils.ts index 1092787a..86d3d3da 100644 --- a/test/batch/talentLayerIdUtils.ts +++ b/test/batch/talentLayerIdUtils.ts @@ -42,7 +42,7 @@ async function deployAndSetup( } -describe.only('TalentLayerIdUtils', function () { +describe('TalentLayerIdUtils', function () { let alice: SignerWithAddress, backendDelegate: SignerWithAddress, platformOwner: SignerWithAddress, diff --git a/test/manual/audit.ts b/test/manual/audit.ts index 0f75f7d2..1a91b295 100644 --- a/test/manual/audit.ts +++ b/test/manual/audit.ts @@ -112,7 +112,7 @@ describe('Audit test', function () { await expect(tx).to.revertedWith('nothing to claim') }) - it.only('must prevent to create a proposal on a non existent Service', async function () { + it('must prevent to create a proposal on a non existent Service', async function () { const aliceTlId = await talentLayerID.connect(alice).ids(alice.address) const signature = await getSignatureForService(carol, aliceTlId.toNumber(), 0, cid) await talentLayerService From 800696899f0d437188023a0b02e33df305c76373 Mon Sep 17 00:00:00 2001 From: "Quentin D.C" Date: Tue, 9 Apr 2024 08:35:29 +0100 Subject: [PATCH 2/5] Added transferFrom funciton to PlatformId contract --- contracts/TalentLayerPlatformID.sol | 16 ++++++++--- test/batch/fullWorkflow.ts | 42 ++++++++++++++++------------- test/batch/talentLayerIdUtils.ts | 1 - 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/contracts/TalentLayerPlatformID.sol b/contracts/TalentLayerPlatformID.sol index 6a72ebea..2ffc63e8 100644 --- a/contracts/TalentLayerPlatformID.sol +++ b/contracts/TalentLayerPlatformID.sol @@ -502,10 +502,18 @@ contract TalentLayerPlatformID is ERC721Upgradeable, AccessControlUpgradeable, U } /** - * @dev Override to prevent token transfer. - */ - function _transfer(address, address, uint256) internal virtual override(ERC721Upgradeable) { - revert("Token transfer is not allowed"); + * @dev Implementation of the {ERC721Upgradeable-transferFrom} function. + * @notice Only one Id allowed per account. + */ + function transferFrom( + address from, + address to, + uint256 tokenId + ) public virtual override { + require(balanceOf(to) == 0, "Recipient already has a Platform ID"); + require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); + + _transfer(from, to, tokenId); } /** diff --git a/test/batch/fullWorkflow.ts b/test/batch/fullWorkflow.ts index 87921140..a86d1cd5 100644 --- a/test/batch/fullWorkflow.ts +++ b/test/batch/fullWorkflow.ts @@ -155,24 +155,6 @@ describe('TalentLayer protocol global testing', function () { expect(alicePlatformData.proposalPostingFee).to.be.equal(100) }) - it('Alice should not be able to transfer her PlatformId Id to Bob', async function () { - await expect( - talentLayerPlatformID.connect(alice).transferFrom(alice.address, bob.address, 1), - ).to.be.revertedWith('Token transfer is not allowed') - - await expect( - talentLayerPlatformID - .connect(alice) - ['safeTransferFrom(address,address,uint256)'](alice.address, bob.address, 1), - ).to.be.revertedWith('Token transfer is not allowed') - - await expect( - talentLayerPlatformID - .connect(alice) - ['safeTransferFrom(address,address,uint256,bytes)'](alice.address, bob.address, 1, []), - ).to.be.revertedWith('Token transfer is not allowed') - }) - it('Alice should not be able to mint a new PlatformId ID', async function () { await expect(talentLayerPlatformID.connect(alice).mint('SecPlatId')).to.be.revertedWith( 'Platform already has a Platform ID', @@ -350,6 +332,30 @@ describe('TalentLayer protocol global testing', function () { ).not.to.be.revertedWithCustomError(talentLayerPlatformID, 'HandleContainsInvalidCharacters') }) + it('Grace can not transfer her platform ID to someone who already owns one', async function () { + const gracePlatformId = await talentLayerPlatformID.ids(grace.address); + expect( + talentLayerPlatformID + .connect(grace) + .transferFrom(grace.address, bob.address, gracePlatformId), + ).to.be.revertedWith('Recipient already has a Platform ID') + }) + + it('Grace can transfer her platform ID to someone who does not own one', async function () { + const gracePlatformId = await talentLayerPlatformID.ids(grace.address); + expect( + await talentLayerPlatformID + .connect(grace) + .transferFrom(grace.address, carol.address, gracePlatformId), + ).not.to.be.revertedWith('Recipient already has a Platform ID') + + const graceBalance = await talentLayerPlatformID.balanceOf(grace.address) + expect(graceBalance).to.be.equal(0) + + const carolBalance = await talentLayerPlatformID.balanceOf(carol.address) + expect(carolBalance).to.be.equal(1) + }) + it("The deployer can withdraw the contract's balance", async function () { const deployerBalanceBefore = await deployer.getBalance() const contractBalanceBefore = await ethers.provider.getBalance(talentLayerPlatformID.address) diff --git a/test/batch/talentLayerIdUtils.ts b/test/batch/talentLayerIdUtils.ts index 86d3d3da..f24fa5da 100644 --- a/test/batch/talentLayerIdUtils.ts +++ b/test/batch/talentLayerIdUtils.ts @@ -70,7 +70,6 @@ describe('TalentLayerIdUtils', function () { // Assert that the token balance of alice is 1 const aliceBalance = await talentLayerId.balanceOf(alice.address) - console.log(aliceBalance.toString()) expect(aliceBalance).to.equal(1) // Assert that backendDelegate is delegate of alice From c2f596056f1356d20d74696ca9bfb9a66bb86e3b Mon Sep 17 00:00:00 2001 From: "Quentin D.C" Date: Tue, 9 Apr 2024 14:45:58 +0100 Subject: [PATCH 3/5] Lint & format --- contracts/TalentLayerIdUtils.sol | 19 ++-- contracts/TalentLayerPlatformID.sol | 6 +- .../tasks/deploy/deploy-talent-layer-utils.ts | 22 +++-- test/batch/fullWorkflow.ts | 4 +- test/batch/talentLayerIdUtils.ts | 93 +++++++++---------- 5 files changed, 75 insertions(+), 69 deletions(-) diff --git a/contracts/TalentLayerIdUtils.sol b/contracts/TalentLayerIdUtils.sol index 5eecba10..351b92a6 100644 --- a/contracts/TalentLayerIdUtils.sol +++ b/contracts/TalentLayerIdUtils.sol @@ -5,7 +5,6 @@ import {TalentLayerID} from "./TalentLayerID.sol"; import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; contract TalentLayerIdUtils is IERC721Receiver { - // =========================== Mappings & Variables ============================== /** @@ -13,16 +12,20 @@ contract TalentLayerIdUtils is IERC721Receiver { */ TalentLayerID public talentLayerIdContract; - // =========================== Initializers ============================== - constructor(address _talentLayerIDAddress){ + constructor(address _talentLayerIDAddress) { talentLayerIdContract = TalentLayerID(_talentLayerIDAddress); } // =========================== User functions ============================== - function mintDelegateAndTransfer(address _to, address _delegateAddress, uint256 _platformId, string calldata _handle) external payable { + function mintDelegateAndTransfer( + address _to, + address _delegateAddress, + uint256 _platformId, + string calldata _handle + ) external payable { // Mint TLID token uint256 tokenId = talentLayerIdContract.mint{value: msg.value}(_platformId, _handle); // Add address as delegate @@ -33,8 +36,12 @@ contract TalentLayerIdUtils is IERC721Receiver { // =========================== Overrides ============================== - function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external pure override returns (bytes4) { + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) external pure override returns (bytes4) { return this.onERC721Received.selector; } - } diff --git a/contracts/TalentLayerPlatformID.sol b/contracts/TalentLayerPlatformID.sol index 2ffc63e8..3e269c0b 100644 --- a/contracts/TalentLayerPlatformID.sol +++ b/contracts/TalentLayerPlatformID.sol @@ -505,11 +505,7 @@ contract TalentLayerPlatformID is ERC721Upgradeable, AccessControlUpgradeable, U * @dev Implementation of the {ERC721Upgradeable-transferFrom} function. * @notice Only one Id allowed per account. */ - function transferFrom( - address from, - address to, - uint256 tokenId - ) public virtual override { + function transferFrom(address from, address to, uint256 tokenId) public virtual override { require(balanceOf(to) == 0, "Recipient already has a Platform ID"); require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); diff --git a/scripts/tasks/deploy/deploy-talent-layer-utils.ts b/scripts/tasks/deploy/deploy-talent-layer-utils.ts index ca1daff5..3133e1cf 100644 --- a/scripts/tasks/deploy/deploy-talent-layer-utils.ts +++ b/scripts/tasks/deploy/deploy-talent-layer-utils.ts @@ -1,7 +1,11 @@ -import {formatEther} from 'ethers/lib/utils' -import {task} from 'hardhat/config' -import {DeploymentProperty, getDeploymentProperty, setDeploymentProperty} from '../../../.deployment/deploymentManager' -import {verifyAddress} from './utils' +import { formatEther } from 'ethers/lib/utils' +import { task } from 'hardhat/config' +import { + DeploymentProperty, + getDeploymentProperty, + setDeploymentProperty, +} from '../../../.deployment/deploymentManager' +import { verifyAddress } from './utils' /** * @notice Task created only for test purposes of the upgradable process @@ -26,14 +30,14 @@ task('deploy-talent-layer-utils', 'Deploy utils contract') await run('compile') const talentLayerID = await ethers.getContractAt( - 'TalentLayerID', - getDeploymentProperty(network.name, DeploymentProperty.TalentLayerID), + 'TalentLayerID', + getDeploymentProperty(network.name, DeploymentProperty.TalentLayerID), ) - console.log('Deploying TalentLayerIdUtils...'); + console.log('Deploying TalentLayerIdUtils...') - const TalentLayerIdUtils = await ethers.getContractFactory("TalentLayerIdUtils"); - const talentLayerIdUtils = await TalentLayerIdUtils.deploy(talentLayerID.address); + const TalentLayerIdUtils = await ethers.getContractFactory('TalentLayerIdUtils') + const talentLayerIdUtils = await TalentLayerIdUtils.deploy(talentLayerID.address) await talentLayerIdUtils.deployTransaction.wait(1) diff --git a/test/batch/fullWorkflow.ts b/test/batch/fullWorkflow.ts index a86d1cd5..20c0c0d6 100644 --- a/test/batch/fullWorkflow.ts +++ b/test/batch/fullWorkflow.ts @@ -333,7 +333,7 @@ describe('TalentLayer protocol global testing', function () { }) it('Grace can not transfer her platform ID to someone who already owns one', async function () { - const gracePlatformId = await talentLayerPlatformID.ids(grace.address); + const gracePlatformId = await talentLayerPlatformID.ids(grace.address) expect( talentLayerPlatformID .connect(grace) @@ -342,7 +342,7 @@ describe('TalentLayer protocol global testing', function () { }) it('Grace can transfer her platform ID to someone who does not own one', async function () { - const gracePlatformId = await talentLayerPlatformID.ids(grace.address); + const gracePlatformId = await talentLayerPlatformID.ids(grace.address) expect( await talentLayerPlatformID .connect(grace) diff --git a/test/batch/talentLayerIdUtils.ts b/test/batch/talentLayerIdUtils.ts index f24fa5da..619e5e3a 100644 --- a/test/batch/talentLayerIdUtils.ts +++ b/test/batch/talentLayerIdUtils.ts @@ -1,26 +1,23 @@ -import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers' -import {ethers} from 'hardhat' -import {expect} from 'chai' -import {TalentLayerID, TalentLayerIdUtils, TalentLayerPlatformID,} from '../../typechain-types' -import {MintStatus,} from '../utils/constant' -import {deploy} from '../utils/deploy' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { ethers } from 'hardhat' +import { expect } from 'chai' +import { TalentLayerID, TalentLayerIdUtils, TalentLayerPlatformID } from '../../typechain-types' +import { MintStatus } from '../utils/constant' +import { deploy } from '../utils/deploy' /** * Deploys contract and sets up the context for TalentLayerIdUtils tests. * @returns the deployed contracts */ -async function deployAndSetup( -): Promise<[TalentLayerPlatformID, TalentLayerID, TalentLayerIdUtils]> { +async function deployAndSetup(): Promise< + [TalentLayerPlatformID, TalentLayerID, TalentLayerIdUtils] +> { const [deployer, alice, backendDelegate, platformOwner] = await ethers.getSigners() - const [ - talentLayerID, - talentLayerPlatformID, - ] = await deploy(false) + const [talentLayerID, talentLayerPlatformID] = await deploy(false) // Deploy TalentLayerIdUtils contract with the address of TalentLayerID - const TalentLayerIdUtils = await ethers.getContractFactory("TalentLayerIdUtils"); - const talentLayerIdUtils = await TalentLayerIdUtils.deploy(talentLayerID.address); - + const TalentLayerIdUtils = await ethers.getContractFactory('TalentLayerIdUtils') + const talentLayerIdUtils = await TalentLayerIdUtils.deploy(talentLayerID.address) // Grant Platform Id Mint role to Deployer and Bob const mintRole = await talentLayerPlatformID.MINT_ROLE() @@ -31,55 +28,57 @@ async function deployAndSetup( await talentLayerPlatformID.connect(deployer).whitelistUser(deployer.address) await talentLayerPlatformID.connect(deployer).mintForAddress(platformName, platformOwner.address) - // Disable whitelist for reserved handles await talentLayerID.connect(deployer).updateMintStatus(MintStatus.PUBLIC) -// Update mint fee - await talentLayerID.connect(deployer).updateMintFee(ethers.utils.parseEther("0.01")) + // Update mint fee + await talentLayerID.connect(deployer).updateMintFee(ethers.utils.parseEther('0.01')) - return [talentLayerPlatformID, talentLayerID, talentLayerIdUtils]; + return [talentLayerPlatformID, talentLayerID, talentLayerIdUtils] } - describe('TalentLayerIdUtils', function () { let alice: SignerWithAddress, backendDelegate: SignerWithAddress, platformOwner: SignerWithAddress, talentLayerPlatformID: TalentLayerPlatformID, talentLayerId: TalentLayerID, - talentLayerIdUtils: TalentLayerIdUtils; + talentLayerIdUtils: TalentLayerIdUtils const platformId = 1 before(async function () { ;[, alice, backendDelegate, platformOwner] = await ethers.getSigners() - ;[talentLayerPlatformID, talentLayerId, talentLayerIdUtils] = - await deployAndSetup() + ;[talentLayerPlatformID, talentLayerId, talentLayerIdUtils] = await deployAndSetup() }) - it('mintDelegateAndTransfer', async function () { - const handle = "pipou"; - const mintFee = await talentLayerId.getHandlePrice(handle); - - await expect( - talentLayerIdUtils.connect(backendDelegate).mintDelegateAndTransfer(alice.address, backendDelegate.address, platformId, 'pip') - ).to.be.reverted - - await talentLayerIdUtils.connect(backendDelegate).mintDelegateAndTransfer(alice.address, backendDelegate.address, platformId, handle, { value: mintFee }) - - // Assert that the token balance of alice is 1 - const aliceBalance = await talentLayerId.balanceOf(alice.address) - expect(aliceBalance).to.equal(1) - - // Assert that backendDelegate is delegate of alice - const aliceTokenId = await talentLayerId.ids(alice.address) - const aliceDelegate = await talentLayerId.isDelegate(aliceTokenId, backendDelegate.address) - expect(aliceDelegate).to.equal(true) - - // Assert backend delegate balance is 0 - const backendDelegateBalance = await talentLayerId.balanceOf(backendDelegate.address) - expect(backendDelegateBalance).to.equal(0) - - }) + it('mintDelegateAndTransfer', async function () { + const handle = 'pipou' + const mintFee = await talentLayerId.getHandlePrice(handle) + + await expect( + talentLayerIdUtils + .connect(backendDelegate) + .mintDelegateAndTransfer(alice.address, backendDelegate.address, platformId, 'pip'), + ).to.be.reverted + + await talentLayerIdUtils + .connect(backendDelegate) + .mintDelegateAndTransfer(alice.address, backendDelegate.address, platformId, handle, { + value: mintFee, + }) + + // Assert that the token balance of alice is 1 + const aliceBalance = await talentLayerId.balanceOf(alice.address) + expect(aliceBalance).to.equal(1) + + // Assert that backendDelegate is delegate of alice + const aliceTokenId = await talentLayerId.ids(alice.address) + const aliceDelegate = await talentLayerId.isDelegate(aliceTokenId, backendDelegate.address) + expect(aliceDelegate).to.equal(true) + + // Assert backend delegate balance is 0 + const backendDelegateBalance = await talentLayerId.balanceOf(backendDelegate.address) + expect(backendDelegateBalance).to.equal(0) + }) }) From f6c40772426e00661c102c69553765edb93a6bd5 Mon Sep 17 00:00:00 2001 From: "Quentin D.C" Date: Tue, 9 Apr 2024 15:10:53 +0100 Subject: [PATCH 4/5] V2 archive --- contracts/archive/TalenrLayerPlatformIdV2.sol | 709 ++++++++++++++++++ 1 file changed, 709 insertions(+) create mode 100644 contracts/archive/TalenrLayerPlatformIdV2.sol diff --git a/contracts/archive/TalenrLayerPlatformIdV2.sol b/contracts/archive/TalenrLayerPlatformIdV2.sol new file mode 100644 index 00000000..32e44a26 --- /dev/null +++ b/contracts/archive/TalenrLayerPlatformIdV2.sol @@ -0,0 +1,709 @@ +// 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 TalentLayerPlatformID 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; + emit ArbitratorAdded(_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; + emit ArbitratorRemoved(_arbitrator); + } + + /** + * @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 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 + * @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); +} From 4b1798ff5dd683465baebf56b6456a48aac01594 Mon Sep 17 00:00:00 2001 From: "Quentin D.C" Date: Tue, 9 Apr 2024 15:53:23 +0100 Subject: [PATCH 5/5] V2 archive - corrected import Added TLPID Migration test --- contracts/archive/TalenrLayerPlatformIdV2.sol | 4 +- test/batch/fullWorkflow.ts | 4 +- test/batch/talentLayerPlatformIdV3.ts | 72 +++++++++++++++++++ 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 test/batch/talentLayerPlatformIdV3.ts diff --git a/contracts/archive/TalenrLayerPlatformIdV2.sol b/contracts/archive/TalenrLayerPlatformIdV2.sol index 32e44a26..19999662 100644 --- a/contracts/archive/TalenrLayerPlatformIdV2.sol +++ b/contracts/archive/TalenrLayerPlatformIdV2.sol @@ -6,13 +6,13 @@ import {Base64Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/Base6 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"; +import {Arbitrator} from "../Arbitrator.sol"; /** * @title Platform ID Contract * @author TalentLayer Team | Website: https://talentlayer.org | Twitter: @talentlayer */ -contract TalentLayerPlatformID is ERC721Upgradeable, AccessControlUpgradeable, UUPSUpgradeable { +contract TalentLayerPlatformIDV2 is ERC721Upgradeable, AccessControlUpgradeable, UUPSUpgradeable { using CountersUpgradeable for CountersUpgradeable.Counter; uint8 constant MIN_HANDLE_LENGTH = 5; diff --git a/test/batch/fullWorkflow.ts b/test/batch/fullWorkflow.ts index 20c0c0d6..99ad043b 100644 --- a/test/batch/fullWorkflow.ts +++ b/test/batch/fullWorkflow.ts @@ -332,7 +332,7 @@ describe('TalentLayer protocol global testing', function () { ).not.to.be.revertedWithCustomError(talentLayerPlatformID, 'HandleContainsInvalidCharacters') }) - it('Grace can not transfer her platform ID to someone who already owns one', async function () { + it('Grace can not transfer her platform ID to Bob who already owns one', async function () { const gracePlatformId = await talentLayerPlatformID.ids(grace.address) expect( talentLayerPlatformID @@ -341,7 +341,7 @@ describe('TalentLayer protocol global testing', function () { ).to.be.revertedWith('Recipient already has a Platform ID') }) - it('Grace can transfer her platform ID to someone who does not own one', async function () { + it('Grace can transfer her platform ID to Carol who does not own one', async function () { const gracePlatformId = await talentLayerPlatformID.ids(grace.address) expect( await talentLayerPlatformID diff --git a/test/batch/talentLayerPlatformIdV3.ts b/test/batch/talentLayerPlatformIdV3.ts new file mode 100644 index 00000000..87d756bc --- /dev/null +++ b/test/batch/talentLayerPlatformIdV3.ts @@ -0,0 +1,72 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { upgrades } = require('hardhat') +import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers' +import {expect} from 'chai' +import {ethers} from 'hardhat' +import {TalentLayerPlatformID, TalentLayerPlatformIDV2} from '../../typechain-types' +import {deploy} from '../utils/deploy' + +/** + * Deploys contracts and sets up the context for TalentLayerPlatformId contract. + * @returns the deployed contract + */ +async function deployAndSetup(): Promise<[TalentLayerPlatformIDV2]> { + const [deployer, alice, bob] = await ethers.getSigners() + const [, talentLayerPlatformID] = await deploy(false) + + // Deployer mints Platform Id for Carol + await talentLayerPlatformID.connect(deployer).whitelistUser(deployer.address) + await talentLayerPlatformID.connect(deployer).mintForAddress('pigeon-club', alice.address) + await talentLayerPlatformID.connect(deployer).mintForAddress('racoon-corp', bob.address) + + // Upgrade to TalentLayerPlatformIDV2 + const TalentLayerPlatformIDV2 = await ethers.getContractFactory('TalentLayerPlatformIDV2') + const talentLayerPlatformIDV2 = await upgrades.upgradeProxy(talentLayerPlatformID.address, TalentLayerPlatformIDV2) + + + return [talentLayerPlatformIDV2] +} + +describe('TalentLayerPlatformID V2 migration testing', function () { + let alice: SignerWithAddress, + bob: SignerWithAddress, + carol: SignerWithAddress, + talentLayerPlatformIDV2: TalentLayerPlatformIDV2, + talentLayerPlatformIDV3: TalentLayerPlatformID + + before(async function () { + [, alice, bob, carol] = await ethers.getSigners(); + [talentLayerPlatformIDV2] = await deployAndSetup(); + }) + + describe('TalentLayerPlatformIDV2', async function () { + it('Should not allow the transfer of PlatformId', async function () { + expect(talentLayerPlatformIDV2.connect(alice).transferFrom(alice.address, carol.address, 1)).to.be.revertedWith('Token transfer is not allowed'); + }) + }) + + describe('Migrate to V3', async function () { + it('Should deploy the V3 keeping the same address', async function () { + const TalentLayerPlatformID = await ethers.getContractFactory('TalentLayerPlatformID') + talentLayerPlatformIDV3 = await upgrades.upgradeProxy(talentLayerPlatformIDV2.address, TalentLayerPlatformID) + expect(talentLayerPlatformIDV3.address).to.equal(talentLayerPlatformIDV2.address) + }) + + it('Alice can not transfer her platform ID to Bob who already owns one', async function () { + const alicePlatformId = await talentLayerPlatformIDV3.ids(alice.address); + expect( + talentLayerPlatformIDV3 + .connect(alice) + .transferFrom(alice.address, bob.address, alicePlatformId), + ).to.be.revertedWith('Recipient already has a Platform ID') + }) + + it('Alice can transfer her platform ID to Carol who does not own one', async function () { + const alicePlatformId = await talentLayerPlatformIDV3.ids(alice.address); + expect( + talentLayerPlatformIDV3 + .transferFrom(alice.address, carol.address, alicePlatformId), + ).not.to.be.revertedWith('Recipient already has a Platform ID') + }) + }) +})