diff --git a/.openzeppelin/polygon-mumbai.json b/.openzeppelin/polygon-mumbai.json index 96dd0d0f..b05e56cb 100644 --- a/.openzeppelin/polygon-mumbai.json +++ b/.openzeppelin/polygon-mumbai.json @@ -25,6 +25,46 @@ "address": "0x4bE920eC3e8552292B2147480111063E0dc36872", "txHash": "0xb2eccc32f9e60d7bbb7b0297ff37ae643a2c0bab4946c86f5ceccbda77e7d03e", "kind": "uups" + }, + { + "address": "0x9Edaa157da3c9963175E806ce0F439F13e4151Fb", + "txHash": "0xd6ee5eed488df7d4d82e808a90b155691d2dd41653e8909af6260ab77ff9f3b5", + "kind": "uups" + }, + { + "address": "0xF5F2CF3Ae1F89a617fD55DC88c255FDEfCAEd72b", + "txHash": "0x8352ee7c8568ec6ed2fb6bf7a6277d3eeb7764cb353db1188764443e7c72a3e6", + "kind": "uups" + }, + { + "address": "0x1FA9fd795ac8a3e4DD267497F4227Ee58FBe0e04", + "txHash": "0x26d37dd00b59523651177456ec0761458a2a80de8d28274e5bcbb318f224478d", + "kind": "uups" + }, + { + "address": "0x65bd3250bF6493b6a0b3f5438A18591Ff803c996", + "txHash": "0xb0887b61e5c159f898c4bd8019b7d89762eb5c36da5068c328ccdb41e740a5a3", + "kind": "uups" + }, + { + "address": "0x57384B08DF816D540D1ac3B436716d092bEbe4Bc", + "txHash": "0x4ff75e98a96cb17382d9d01a8587645be847b0679d882a171ba81a1bd9d3dba3", + "kind": "uups" + }, + { + "address": "0xbd4098244759c72F8A64307e1fe5Ae34b3B818C2", + "txHash": "0x0e97b4ed3da7ede9f907c7340f67843d28c6795f121948fd931aacc6e5030954", + "kind": "uups" + }, + { + "address": "0xc56B8Ca7E9Efd9533E10FaeE0813070aF0d93aAb", + "txHash": "0xc241b9b6200d817c83c5f875366b9fdaf1b5f8e20c70efe8ac4194f43b01fac5", + "kind": "uups" + }, + { + "address": "0x40c8784324Ac487c3E324D2f80727131e53B9965", + "txHash": "0x802535b7c2c7ffcc3cd6f1c69ebbe91cc9deea2842d3e96e9a75008f0191dc1e", + "kind": "uups" } ], "impls": { @@ -2261,9 +2301,9 @@ } } }, - "f9e7916a1c316964ab24794f96ac3ee47b57790f1fb37e7a459ebb7c82af0d2e": { - "address": "0x10f5FA91077E8F0Ce4791F998bFE7233216F7E76", - "txHash": "0x7446df365bae78c03532ab0e66f0b2943fddfb5bc45115fce95e90b5d72cc58b", + "5348791f129964f78be6e5dcfc9ba260df2c51b348774af103e0d5290d79ecb6": { + "address": "0x00821fEaC642fB6227d70F358687805A0aD30CF6", + "txHash": "0x2adb2f4430647694bb3eafbc046d7cdf96af21b2263e86d22e0df06011a2fcb1", "layout": { "solcVersion": "0.8.17", "storage": [ @@ -2273,7 +2313,7 @@ "slot": "0", "type": "t_uint8", "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\Initializable.sol:62", "retypedFrom": "bool" }, { @@ -2282,7 +2322,7 @@ "slot": "0", "type": "t_bool", "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\Initializable.sol:67" }, { "label": "__gap", @@ -2290,207 +2330,183 @@ "slot": "1", "type": "t_array(t_uint256)50_storage", "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "51", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "52", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_trustedForwarders", - "offset": 0, - "slot": "101", - "type": "t_mapping(t_address,t_bool)", - "contract": "ERC2771RecipientUpgradeable", - "src": "contracts/libs/ERC2771RecipientUpgradeable.sol:29" + "src": "@openzeppelin\\contracts-upgradeable\\utils\\ContextUpgradeable.sol:36" }, { "label": "__gap", "offset": 0, - "slot": "102", + "slot": "51", "type": "t_array(t_uint256)50_storage", "contract": "ERC165Upgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + "src": "@openzeppelin\\contracts-upgradeable\\utils\\introspection\\ERC165Upgradeable.sol:41" }, { "label": "_name", "offset": 0, - "slot": "152", + "slot": "101", "type": "t_string_storage", "contract": "ERC721Upgradeable", - "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:25" + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:25" }, { "label": "_symbol", "offset": 0, - "slot": "153", + "slot": "102", "type": "t_string_storage", "contract": "ERC721Upgradeable", - "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:28" + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:28" }, { "label": "_owners", "offset": 0, - "slot": "154", + "slot": "103", "type": "t_mapping(t_uint256,t_address)", "contract": "ERC721Upgradeable", - "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:31" + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:31" }, { "label": "_balances", "offset": 0, - "slot": "155", + "slot": "104", "type": "t_mapping(t_address,t_uint256)", "contract": "ERC721Upgradeable", - "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:34" + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:34" }, { "label": "_tokenApprovals", "offset": 0, - "slot": "156", + "slot": "105", "type": "t_mapping(t_uint256,t_address)", "contract": "ERC721Upgradeable", - "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:37" + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:37" }, { "label": "_operatorApprovals", "offset": 0, - "slot": "157", + "slot": "106", "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", "contract": "ERC721Upgradeable", - "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:40" + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:40" }, { "label": "__gap", "offset": 0, - "slot": "158", + "slot": "107", "type": "t_array(t_uint256)44_storage", "contract": "ERC721Upgradeable", - "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:514" + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:514" + }, + { + "label": "_roles", + "offset": 0, + "slot": "151", + "type": "t_mapping(t_bytes32,t_struct(RoleData)58_storage)", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\access\\AccessControlUpgradeable.sol:61" }, { "label": "__gap", "offset": 0, - "slot": "202", + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\access\\AccessControlUpgradeable.sol:259" + }, + { + "label": "__gap", + "offset": 0, + "slot": "201", "type": "t_array(t_uint256)50_storage", "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\ERC1967\\ERC1967UpgradeUpgradeable.sol:211" }, { "label": "__gap", "offset": 0, - "slot": "252", + "slot": "251", "type": "t_array(t_uint256)50_storage", "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\UUPSUpgradeable.sol:107" }, { - "label": "talentLayerPlatformIdContract", + "label": "takenNames", + "offset": 0, + "slot": "301", + "type": "t_mapping(t_string_memory_ptr,t_bool)", + "contract": "TalentLayerPlatformID", + "src": "contracts\\TalentLayerPlatformID.sol:65" + }, + { + "label": "platforms", "offset": 0, "slot": "302", - "type": "t_contract(ITalentLayerPlatformID)7478", - "contract": "TalentLayerID", - "src": "contracts/TalentLayerID.sol:70" + "type": "t_mapping(t_uint256,t_struct(Platform)12988_storage)", + "contract": "TalentLayerPlatformID", + "src": "contracts\\TalentLayerPlatformID.sol:70" }, { - "label": "takenHandles", + "label": "validArbitrators", "offset": 0, "slot": "303", - "type": "t_mapping(t_string_memory_ptr,t_bool)", - "contract": "TalentLayerID", - "src": "contracts/TalentLayerID.sol:75" + "type": "t_mapping(t_address,t_bool)", + "contract": "TalentLayerPlatformID", + "src": "contracts\\TalentLayerPlatformID.sol:75" }, { - "label": "profiles", + "label": "whitelist", "offset": 0, "slot": "304", - "type": "t_mapping(t_uint256,t_struct(Profile)4707_storage)", - "contract": "TalentLayerID", - "src": "contracts/TalentLayerID.sol:80" + "type": "t_mapping(t_address,t_bool)", + "contract": "TalentLayerPlatformID", + "src": "contracts\\TalentLayerPlatformID.sol:80" }, { - "label": "ids", + "label": "internalArbitrators", "offset": 0, "slot": "305", - "type": "t_mapping(t_address,t_uint256)", - "contract": "TalentLayerID", - "src": "contracts/TalentLayerID.sol:85" + "type": "t_mapping(t_address,t_bool)", + "contract": "TalentLayerPlatformID", + "src": "contracts\\TalentLayerPlatformID.sol:86" }, { - "label": "mintFee", + "label": "ids", "offset": 0, "slot": "306", - "type": "t_uint256", - "contract": "TalentLayerID", - "src": "contracts/TalentLayerID.sol:90" + "type": "t_mapping(t_address,t_uint256)", + "contract": "TalentLayerPlatformID", + "src": "contracts\\TalentLayerPlatformID.sol:91" }, { - "label": "nextProfileId", + "label": "mintFee", "offset": 0, "slot": "307", - "type": "t_struct(Counter)2768_storage", - "contract": "TalentLayerID", - "src": "contracts/TalentLayerID.sol:95" + "type": "t_uint256", + "contract": "TalentLayerPlatformID", + "src": "contracts\\TalentLayerPlatformID.sol:96" }, { - "label": "delegates", + "label": "minArbitrationFeeTimeout", "offset": 0, "slot": "308", - "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))", - "contract": "TalentLayerID", - "src": "contracts/TalentLayerID.sol:100" + "type": "t_uint256", + "contract": "TalentLayerPlatformID", + "src": "contracts\\TalentLayerPlatformID.sol:106" }, { - "label": "whitelistMerkleRoot", + "label": "nextPlatformId", "offset": 0, "slot": "309", - "type": "t_bytes32", - "contract": "TalentLayerID", - "src": "contracts/TalentLayerID.sol:105" + "type": "t_struct(Counter)3290_storage", + "contract": "TalentLayerPlatformID", + "src": "contracts\\TalentLayerPlatformID.sol:111" }, { "label": "mintStatus", "offset": 0, "slot": "310", - "type": "t_enum(MintStatus)4698", - "contract": "TalentLayerID", - "src": "contracts/TalentLayerID.sol:110" - }, - { - "label": "shortHandlesMaxPrice", - "offset": 0, - "slot": "311", - "type": "t_uint256", - "contract": "TalentLayerID", - "src": "contracts/TalentLayerID.sol:115" - }, - { - "label": "hasActivity", - "offset": 0, - "slot": "312", - "type": "t_mapping(t_uint256,t_bool)", - "contract": "TalentLayerID", - "src": "contracts/TalentLayerID.sol:120" - }, - { - "label": "isServiceContract", - "offset": 0, - "slot": "313", - "type": "t_mapping(t_address,t_bool)", - "contract": "TalentLayerID", - "src": "contracts/TalentLayerID.sol:125" + "type": "t_enum(MintStatus)12964", + "contract": "TalentLayerPlatformID", + "src": "contracts\\TalentLayerPlatformID.sol:116" } ], "types": { @@ -2518,12 +2534,16 @@ "label": "bytes32", "numberOfBytes": "32" }, - "t_contract(ITalentLayerPlatformID)7478": { - "label": "contract ITalentLayerPlatformID", + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_contract(Arbitrator)7789": { + "label": "contract Arbitrator", "numberOfBytes": "20" }, - "t_enum(MintStatus)4698": { - "label": "enum TalentLayerID.MintStatus", + "t_enum(MintStatus)12964": { + "label": "enum TalentLayerPlatformID.MintStatus", "members": [ "ON_PAUSE", "ONLY_WHITELIST", @@ -2543,6 +2563,10 @@ "label": "mapping(address => uint256)", "numberOfBytes": "32" }, + "t_mapping(t_bytes32,t_struct(RoleData)58_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, "t_mapping(t_string_memory_ptr,t_bool)": { "label": "mapping(string => bool)", "numberOfBytes": "32" @@ -2551,16 +2575,8 @@ "label": "mapping(uint256 => address)", "numberOfBytes": "32" }, - "t_mapping(t_uint256,t_bool)": { - "label": "mapping(uint256 => bool)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint256,t_mapping(t_address,t_bool))": { - "label": "mapping(uint256 => mapping(address => bool))", - "numberOfBytes": "32" - }, - "t_mapping(t_uint256,t_struct(Profile)4707_storage)": { - "label": "mapping(uint256 => struct TalentLayerID.Profile)", + "t_mapping(t_uint256,t_struct(Platform)12988_storage)": { + "label": "mapping(uint256 => struct TalentLayerPlatformID.Platform)", "numberOfBytes": "32" }, "t_string_memory_ptr": { @@ -2571,7 +2587,7 @@ "label": "string", "numberOfBytes": "32" }, - "t_struct(Counter)2768_storage": { + "t_struct(Counter)3290_storage": { "label": "struct CountersUpgradeable.Counter", "members": [ { @@ -2583,8 +2599,8 @@ ], "numberOfBytes": "32" }, - "t_struct(Profile)4707_storage": { - "label": "struct TalentLayerID.Profile", + "t_struct(Platform)12988_storage": { + "label": "struct TalentLayerPlatformID.Platform", "members": [ { "label": "id", @@ -2593,25 +2609,1560 @@ "slot": "0" }, { - "label": "handle", + "label": "name", "type": "t_string_storage", "offset": 0, "slot": "1" }, { - "label": "platformId", - "type": "t_uint256", + "label": "dataUri", + "type": "t_string_storage", "offset": 0, "slot": "2" }, { - "label": "dataUri", - "type": "t_string_storage", + "label": "originServiceFeeRate", + "type": "t_uint16", "offset": 0, "slot": "3" - } - ], - "numberOfBytes": "128" + }, + { + "label": "originValidatedProposalFeeRate", + "type": "t_uint16", + "offset": 2, + "slot": "3" + }, + { + "label": "servicePostingFee", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "proposalPostingFee", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "arbitrator", + "type": "t_contract(Arbitrator)7789", + "offset": 0, + "slot": "6" + }, + { + "label": "arbitratorExtraData", + "type": "t_bytes_storage", + "offset": 0, + "slot": "7" + }, + { + "label": "arbitrationFeeTimeout", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "signer", + "type": "t_address", + "offset": 0, + "slot": "9" + } + ], + "numberOfBytes": "320" + }, + "t_struct(RoleData)58_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "f9e7916a1c316964ab24794f96ac3ee47b57790f1fb37e7a459ebb7c82af0d2e": { + "address": "0x2c4d94b4A4cb80d6241cAc36Df4173CFdB285093", + "txHash": "0x2f047ceb43c62e4d1fb26fcf77a2f026c86fc6c69f53db5aea0fab6479c44e09", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\utils\\ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\access\\OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\access\\OwnableUpgradeable.sol:94" + }, + { + "label": "_trustedForwarders", + "offset": 0, + "slot": "101", + "type": "t_mapping(t_address,t_bool)", + "contract": "ERC2771RecipientUpgradeable", + "src": "contracts\\libs\\ERC2771RecipientUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\utils\\introspection\\ERC165Upgradeable.sol:41" + }, + { + "label": "_name", + "offset": 0, + "slot": "152", + "type": "t_string_storage", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:25" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "153", + "type": "t_string_storage", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:28" + }, + { + "label": "_owners", + "offset": 0, + "slot": "154", + "type": "t_mapping(t_uint256,t_address)", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:31" + }, + { + "label": "_balances", + "offset": 0, + "slot": "155", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:34" + }, + { + "label": "_tokenApprovals", + "offset": 0, + "slot": "156", + "type": "t_mapping(t_uint256,t_address)", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:37" + }, + { + "label": "_operatorApprovals", + "offset": 0, + "slot": "157", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:40" + }, + { + "label": "__gap", + "offset": 0, + "slot": "158", + "type": "t_array(t_uint256)44_storage", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:514" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\ERC1967\\ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\UUPSUpgradeable.sol:107" + }, + { + "label": "talentLayerPlatformIdContract", + "offset": 0, + "slot": "302", + "type": "t_contract(ITalentLayerPlatformID)22226", + "contract": "TalentLayerID", + "src": "contracts\\TalentLayerID.sol:70" + }, + { + "label": "takenHandles", + "offset": 0, + "slot": "303", + "type": "t_mapping(t_string_memory_ptr,t_bool)", + "contract": "TalentLayerID", + "src": "contracts\\TalentLayerID.sol:75" + }, + { + "label": "profiles", + "offset": 0, + "slot": "304", + "type": "t_mapping(t_uint256,t_struct(Profile)11725_storage)", + "contract": "TalentLayerID", + "src": "contracts\\TalentLayerID.sol:80" + }, + { + "label": "ids", + "offset": 0, + "slot": "305", + "type": "t_mapping(t_address,t_uint256)", + "contract": "TalentLayerID", + "src": "contracts\\TalentLayerID.sol:85" + }, + { + "label": "mintFee", + "offset": 0, + "slot": "306", + "type": "t_uint256", + "contract": "TalentLayerID", + "src": "contracts\\TalentLayerID.sol:90" + }, + { + "label": "nextProfileId", + "offset": 0, + "slot": "307", + "type": "t_struct(Counter)3290_storage", + "contract": "TalentLayerID", + "src": "contracts\\TalentLayerID.sol:95" + }, + { + "label": "delegates", + "offset": 0, + "slot": "308", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))", + "contract": "TalentLayerID", + "src": "contracts\\TalentLayerID.sol:100" + }, + { + "label": "whitelistMerkleRoot", + "offset": 0, + "slot": "309", + "type": "t_bytes32", + "contract": "TalentLayerID", + "src": "contracts\\TalentLayerID.sol:105" + }, + { + "label": "mintStatus", + "offset": 0, + "slot": "310", + "type": "t_enum(MintStatus)11716", + "contract": "TalentLayerID", + "src": "contracts\\TalentLayerID.sol:110" + }, + { + "label": "shortHandlesMaxPrice", + "offset": 0, + "slot": "311", + "type": "t_uint256", + "contract": "TalentLayerID", + "src": "contracts\\TalentLayerID.sol:115" + }, + { + "label": "hasActivity", + "offset": 0, + "slot": "312", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "TalentLayerID", + "src": "contracts\\TalentLayerID.sol:120" + }, + { + "label": "isServiceContract", + "offset": 0, + "slot": "313", + "type": "t_mapping(t_address,t_bool)", + "contract": "TalentLayerID", + "src": "contracts\\TalentLayerID.sol:125" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)44_storage": { + "label": "uint256[44]", + "numberOfBytes": "1408" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(ITalentLayerPlatformID)22226": { + "label": "contract ITalentLayerPlatformID", + "numberOfBytes": "20" + }, + "t_enum(MintStatus)11716": { + "label": "enum TalentLayerID.MintStatus", + "members": [ + "ON_PAUSE", + "ONLY_WHITELIST", + "PUBLIC" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_string_memory_ptr,t_bool)": { + "label": "mapping(string => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_address)": { + "label": "mapping(uint256 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_address,t_bool))": { + "label": "mapping(uint256 => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Profile)11725_storage)": { + "label": "mapping(uint256 => struct TalentLayerID.Profile)", + "numberOfBytes": "32" + }, + "t_string_memory_ptr": { + "label": "string", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Counter)3290_storage": { + "label": "struct CountersUpgradeable.Counter", + "members": [ + { + "label": "_value", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Profile)11725_storage": { + "label": "struct TalentLayerID.Profile", + "members": [ + { + "label": "id", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "handle", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "platformId", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "dataUri", + "type": "t_string_storage", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "fff83eb49b2b7484d73644074ce0abcc8ea1ec5506e30ab2473bc1a895b0c4c8": { + "address": "0xfBC702bFDE788B3df34d9aF93FfaD30C7B1D341f", + "txHash": "0xcd960e27b6ee76144df4a04aae0196913cbc389f36038dd971db7cf7fd86fb26", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\utils\\ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\access\\OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\access\\OwnableUpgradeable.sol:94" + }, + { + "label": "_trustedForwarders", + "offset": 0, + "slot": "101", + "type": "t_mapping(t_address,t_bool)", + "contract": "ERC2771RecipientUpgradeable", + "src": "contracts\\libs\\ERC2771RecipientUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\ERC1967\\ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\UUPSUpgradeable.sol:107" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\utils\\introspection\\ERC165Upgradeable.sol:41" + }, + { + "label": "_roles", + "offset": 0, + "slot": "252", + "type": "t_mapping(t_bytes32,t_struct(RoleData)58_storage)", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\access\\AccessControlUpgradeable.sol:61" + }, + { + "label": "__gap", + "offset": 0, + "slot": "253", + "type": "t_array(t_uint256)49_storage", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\access\\AccessControlUpgradeable.sol:259" + }, + { + "label": "nextServiceId", + "offset": 0, + "slot": "302", + "type": "t_uint256", + "contract": "TalentLayerService", + "src": "contracts\\TalentLayerService.sol:187" + }, + { + "label": "tlId", + "offset": 0, + "slot": "303", + "type": "t_contract(ITalentLayerID)22004", + "contract": "TalentLayerService", + "src": "contracts\\TalentLayerService.sol:192" + }, + { + "label": "talentLayerPlatformIdContract", + "offset": 0, + "slot": "304", + "type": "t_contract(ITalentLayerPlatformID)22226", + "contract": "TalentLayerService", + "src": "contracts\\TalentLayerService.sol:197" + }, + { + "label": "services", + "offset": 0, + "slot": "305", + "type": "t_mapping(t_uint256,t_struct(Service)14806_storage)", + "contract": "TalentLayerService", + "src": "contracts\\TalentLayerService.sol:202" + }, + { + "label": "proposals", + "offset": 0, + "slot": "306", + "type": "t_mapping(t_uint256,t_mapping(t_uint256,t_struct(Proposal)14824_storage))", + "contract": "TalentLayerService", + "src": "contracts\\TalentLayerService.sol:207" + }, + { + "label": "serviceNonce", + "offset": 0, + "slot": "307", + "type": "t_mapping(t_uint256,t_uint256)", + "contract": "TalentLayerService", + "src": "contracts\\TalentLayerService.sol:212" + }, + { + "label": "proposalNonce", + "offset": 0, + "slot": "308", + "type": "t_mapping(t_uint256,t_uint256)", + "contract": "TalentLayerService", + "src": "contracts\\TalentLayerService.sol:217" + }, + { + "label": "allowedTokenList", + "offset": 0, + "slot": "309", + "type": "t_mapping(t_address,t_struct(AllowedToken)14829_storage)", + "contract": "TalentLayerService", + "src": "contracts\\TalentLayerService.sol:222" + }, + { + "label": "minCompletionPercentage", + "offset": 0, + "slot": "310", + "type": "t_uint256", + "contract": "TalentLayerService", + "src": "contracts\\TalentLayerService.sol:227" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(ITalentLayerID)22004": { + "label": "contract ITalentLayerID", + "numberOfBytes": "20" + }, + "t_contract(ITalentLayerPlatformID)22226": { + "label": "contract ITalentLayerPlatformID", + "numberOfBytes": "20" + }, + "t_enum(ProposalStatus)14788": { + "label": "enum TalentLayerService.ProposalStatus", + "members": [ + "Pending", + "Validated" + ], + "numberOfBytes": "1" + }, + "t_enum(Status)14785": { + "label": "enum TalentLayerService.Status", + "members": [ + "Opened", + "Confirmed", + "Finished", + "Cancelled", + "Uncompleted" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(AllowedToken)14829_storage)": { + "label": "mapping(address => struct TalentLayerService.AllowedToken)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)58_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_uint256,t_struct(Proposal)14824_storage))": { + "label": "mapping(uint256 => mapping(uint256 => struct TalentLayerService.Proposal))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Proposal)14824_storage)": { + "label": "mapping(uint256 => struct TalentLayerService.Proposal)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Service)14806_storage)": { + "label": "mapping(uint256 => struct TalentLayerService.Service)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(AllowedToken)14829_storage": { + "label": "struct TalentLayerService.AllowedToken", + "members": [ + { + "label": "isWhitelisted", + "type": "t_bool", + "offset": 0, + "slot": "0" + }, + { + "label": "minimumTransactionAmount", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Proposal)14824_storage": { + "label": "struct TalentLayerService.Proposal", + "members": [ + { + "label": "status", + "type": "t_enum(ProposalStatus)14788", + "offset": 0, + "slot": "0" + }, + { + "label": "ownerId", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "rateToken", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "rateAmount", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "platformId", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "dataUri", + "type": "t_string_storage", + "offset": 0, + "slot": "5" + }, + { + "label": "expirationDate", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "referrerId", + "type": "t_uint256", + "offset": 0, + "slot": "7" + } + ], + "numberOfBytes": "256" + }, + "t_struct(RoleData)58_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Service)14806_storage": { + "label": "struct TalentLayerService.Service", + "members": [ + { + "label": "status", + "type": "t_enum(Status)14785", + "offset": 0, + "slot": "0" + }, + { + "label": "ownerId", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "acceptedProposalId", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "dataUri", + "type": "t_string_storage", + "offset": 0, + "slot": "3" + }, + { + "label": "transactionId", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "platformId", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "rateToken", + "type": "t_address", + "offset": 0, + "slot": "6" + }, + { + "label": "referralAmount", + "type": "t_uint256", + "offset": 0, + "slot": "7" + } + ], + "numberOfBytes": "256" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "2a12ac0fce1d139ca3c34a8d201fa199c7657f3adb52ca26ae5fcff7019db3b7": { + "address": "0xc7db42815412e7c5d1E8Ed057Bf8D8Ee2F55e25d", + "txHash": "0xbfa32de0c10cc735435e6efc0dc9bbe95996726210c71ce7e1f22da1725a94ba", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\utils\\ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\access\\OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\access\\OwnableUpgradeable.sol:94" + }, + { + "label": "_trustedForwarders", + "offset": 0, + "slot": "101", + "type": "t_mapping(t_address,t_bool)", + "contract": "ERC2771RecipientUpgradeable", + "src": "contracts\\libs\\ERC2771RecipientUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\utils\\introspection\\ERC165Upgradeable.sol:41" + }, + { + "label": "_name", + "offset": 0, + "slot": "152", + "type": "t_string_storage", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:25" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "153", + "type": "t_string_storage", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:28" + }, + { + "label": "_owners", + "offset": 0, + "slot": "154", + "type": "t_mapping(t_uint256,t_address)", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:31" + }, + { + "label": "_balances", + "offset": 0, + "slot": "155", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:34" + }, + { + "label": "_tokenApprovals", + "offset": 0, + "slot": "156", + "type": "t_mapping(t_uint256,t_address)", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:37" + }, + { + "label": "_operatorApprovals", + "offset": 0, + "slot": "157", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:40" + }, + { + "label": "__gap", + "offset": 0, + "slot": "158", + "type": "t_array(t_uint256)44_storage", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\token\\ERC721\\ERC721Upgradeable.sol:514" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\ERC1967\\ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\UUPSUpgradeable.sol:107" + }, + { + "label": "nextReviewId", + "offset": 0, + "slot": "302", + "type": "t_struct(Counter)3290_storage", + "contract": "TalentLayerReview", + "src": "contracts\\TalentLayerReview.sol:47" + }, + { + "label": "reviews", + "offset": 0, + "slot": "303", + "type": "t_mapping(t_uint256,t_struct(Review)14307_storage)", + "contract": "TalentLayerReview", + "src": "contracts\\TalentLayerReview.sol:52" + }, + { + "label": "hasBuyerBeenReviewed", + "offset": 0, + "slot": "304", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "TalentLayerReview", + "src": "contracts\\TalentLayerReview.sol:57" + }, + { + "label": "hasSellerBeenReviewed", + "offset": 0, + "slot": "305", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "TalentLayerReview", + "src": "contracts\\TalentLayerReview.sol:62" + }, + { + "label": "tlId", + "offset": 0, + "slot": "306", + "type": "t_contract(ITalentLayerID)22004", + "contract": "TalentLayerReview", + "src": "contracts\\TalentLayerReview.sol:67" + }, + { + "label": "talentLayerService", + "offset": 0, + "slot": "307", + "type": "t_contract(ITalentLayerService)22448", + "contract": "TalentLayerReview", + "src": "contracts\\TalentLayerReview.sol:72" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)44_storage": { + "label": "uint256[44]", + "numberOfBytes": "1408" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(ITalentLayerID)22004": { + "label": "contract ITalentLayerID", + "numberOfBytes": "20" + }, + "t_contract(ITalentLayerService)22448": { + "label": "contract ITalentLayerService", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_address)": { + "label": "mapping(uint256 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Review)14307_storage)": { + "label": "mapping(uint256 => struct TalentLayerReview.Review)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Counter)3290_storage": { + "label": "struct CountersUpgradeable.Counter", + "members": [ + { + "label": "_value", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Review)14307_storage": { + "label": "struct TalentLayerReview.Review", + "members": [ + { + "label": "id", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "ownerId", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "dataUri", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "serviceId", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "rating", + "type": "t_uint256", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "9e2b0eada60680f506ea93bebcf255a7d11503e12154d1968434159e21ffff13": { + "address": "0x9F4ab41bb4498B7B92bff27a859ed322E7Af82AB", + "txHash": "0x5f0a82f5ab2aee6b4a6110d11bc9efcda19de89e569fc1ef454d502f2db13873", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\utils\\ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\access\\OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\access\\OwnableUpgradeable.sol:94" + }, + { + "label": "_trustedForwarders", + "offset": 0, + "slot": "101", + "type": "t_mapping(t_address,t_bool)", + "contract": "ERC2771RecipientUpgradeable", + "src": "contracts\\libs\\ERC2771RecipientUpgradeable.sol:29" + }, + { + "label": "_paused", + "offset": 0, + "slot": "102", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\security\\PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "103", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\security\\PausableUpgradeable.sol:116" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\ERC1967\\ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin\\contracts-upgradeable\\proxy\\utils\\UUPSUpgradeable.sol:107" + }, + { + "label": "transactions", + "offset": 0, + "slot": "252", + "type": "t_mapping(t_uint256,t_struct(Transaction)9282_storage)", + "contract": "TalentLayerEscrow", + "src": "contracts\\TalentLayerEscrow.sol:282" + }, + { + "label": "platformIdToTokenToBalance", + "offset": 0, + "slot": "253", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_uint256))", + "contract": "TalentLayerEscrow", + "src": "contracts\\TalentLayerEscrow.sol:291" + }, + { + "label": "talentLayerServiceContract", + "offset": 0, + "slot": "254", + "type": "t_contract(ITalentLayerService)22448", + "contract": "TalentLayerEscrow", + "src": "contracts\\TalentLayerEscrow.sol:296" + }, + { + "label": "talentLayerIdContract", + "offset": 0, + "slot": "255", + "type": "t_contract(ITalentLayerID)22004", + "contract": "TalentLayerEscrow", + "src": "contracts\\TalentLayerEscrow.sol:301" + }, + { + "label": "talentLayerPlatformIdContract", + "offset": 0, + "slot": "256", + "type": "t_contract(ITalentLayerPlatformID)22226", + "contract": "TalentLayerEscrow", + "src": "contracts\\TalentLayerEscrow.sol:306" + }, + { + "label": "protocolWallet", + "offset": 0, + "slot": "257", + "type": "t_address_payable", + "contract": "TalentLayerEscrow", + "src": "contracts\\TalentLayerEscrow.sol:311" + }, + { + "label": "protocolEscrowFeeRate", + "offset": 20, + "slot": "257", + "type": "t_uint16", + "contract": "TalentLayerEscrow", + "src": "contracts\\TalentLayerEscrow.sol:316" + }, + { + "label": "disputeIDtoTransactionID", + "offset": 0, + "slot": "258", + "type": "t_mapping(t_uint256,t_uint256)", + "contract": "TalentLayerEscrow", + "src": "contracts\\TalentLayerEscrow.sol:346" + }, + { + "label": "nextTransactionId", + "offset": 0, + "slot": "259", + "type": "t_struct(Counter)3290_storage", + "contract": "TalentLayerEscrow", + "src": "contracts\\TalentLayerEscrow.sol:351" + }, + { + "label": "referrerIdToTokenToBalance", + "offset": 0, + "slot": "260", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_uint256))", + "contract": "TalentLayerEscrow", + "src": "contracts\\TalentLayerEscrow.sol:359" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_address_payable": { + "label": "address payable", + "numberOfBytes": "20" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_contract(Arbitrator)7789": { + "label": "contract Arbitrator", + "numberOfBytes": "20" + }, + "t_contract(ITalentLayerID)22004": { + "label": "contract ITalentLayerID", + "numberOfBytes": "20" + }, + "t_contract(ITalentLayerPlatformID)22226": { + "label": "contract ITalentLayerPlatformID", + "numberOfBytes": "20" + }, + "t_contract(ITalentLayerService)22448": { + "label": "contract ITalentLayerService", + "numberOfBytes": "20" + }, + "t_enum(Status)9235": { + "label": "enum TalentLayerEscrow.Status", + "members": [ + "NoDispute", + "WaitingSender", + "WaitingReceiver", + "DisputeCreated", + "Resolved" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_address,t_uint256))": { + "label": "mapping(uint256 => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Transaction)9282_storage)": { + "label": "mapping(uint256 => struct TalentLayerEscrow.Transaction)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_struct(Counter)3290_storage": { + "label": "struct CountersUpgradeable.Counter", + "members": [ + { + "label": "_value", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Transaction)9282_storage": { + "label": "struct TalentLayerEscrow.Transaction", + "members": [ + { + "label": "id", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "sender", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "receiver", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "3" + }, + { + "label": "amount", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "releasedAmount", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "serviceId", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "proposalId", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "protocolEscrowFeeRate", + "type": "t_uint16", + "offset": 0, + "slot": "8" + }, + { + "label": "originServiceFeeRate", + "type": "t_uint16", + "offset": 2, + "slot": "8" + }, + { + "label": "originValidatedProposalFeeRate", + "type": "t_uint16", + "offset": 4, + "slot": "8" + }, + { + "label": "arbitrator", + "type": "t_contract(Arbitrator)7789", + "offset": 6, + "slot": "8" + }, + { + "label": "status", + "type": "t_enum(Status)9235", + "offset": 26, + "slot": "8" + }, + { + "label": "disputeId", + "type": "t_uint256", + "offset": 0, + "slot": "9" + }, + { + "label": "senderFee", + "type": "t_uint256", + "offset": 0, + "slot": "10" + }, + { + "label": "receiverFee", + "type": "t_uint256", + "offset": 0, + "slot": "11" + }, + { + "label": "lastInteraction", + "type": "t_uint256", + "offset": 0, + "slot": "12" + }, + { + "label": "arbitratorExtraData", + "type": "t_bytes_storage", + "offset": 0, + "slot": "13" + }, + { + "label": "arbitrationFeeTimeout", + "type": "t_uint256", + "offset": 0, + "slot": "14" + }, + { + "label": "referrerId", + "type": "t_uint256", + "offset": 0, + "slot": "15" + }, + { + "label": "referralAmount", + "type": "t_uint256", + "offset": 0, + "slot": "16" + }, + { + "label": "totalAmount", + "type": "t_uint256", + "offset": 0, + "slot": "17" + } + ], + "numberOfBytes": "576" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" }, "t_uint256": { "label": "uint256", diff --git a/contracts/TalentLayerEscrow.sol b/contracts/TalentLayerEscrow.sol index a3ac1f70..eaa1d710 100644 --- a/contracts/TalentLayerEscrow.sol +++ b/contracts/TalentLayerEscrow.sol @@ -73,7 +73,7 @@ contract TalentLayerEscrow is * @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 amount The amount of the transaction EXCLUDING FEES (will diminish after each release payment) * @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 @@ -88,6 +88,9 @@ contract TalentLayerEscrow is * @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 + * @param referrerId the id of the optional referrer + * @param referralAmount the optional lump sum optional amount to be sent to the referrer + * @param totalAmount The amount of the transaction EXCLUDING FEES (fixed, will not vary) */ struct Transaction { uint256 id; @@ -109,6 +112,9 @@ contract TalentLayerEscrow is uint256 lastInteraction; bytes arbitratorExtraData; uint256 arbitrationFeeTimeout; + uint256 referrerId; + uint256 referralAmount; + uint256 totalAmount; } // =========================== Events ============================== @@ -151,6 +157,14 @@ contract TalentLayerEscrow is */ event FeesClaimed(uint256 _platformId, address indexed _token, uint256 _amount); + /** + * @notice Emitted after a referrer withdraws its balance + * @param _referrerId The TalentLayerID to which the balance is transferred. + * @param _token The address of the token used for the payment. + * @param _amount The amount transferred. + */ + event ReferralAmountClaimed(uint256 _referrerId, address indexed _token, uint256 _amount); + /** * @notice Emitted after an origin service fee is released to a platform's balance * @param _platformId The platform ID. @@ -179,6 +193,20 @@ contract TalentLayerEscrow is uint256 _amount ); + /** + * @notice Emitted after part of a referral amount is released + * @param _referrerId The id of the referrer. + * @param _serviceId The related service ID. + * @param _token The address of the token used for the payment. + * @param _amount The amount released. + */ + event ReferralAmountReleased( + uint256 indexed _referrerId, + 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. @@ -322,6 +350,14 @@ contract TalentLayerEscrow is */ CountersUpgradeable.Counter private nextTransactionId; + /** + * @notice Mapping from referrerId to Token address to Token Balance + * Represents the amount of ETH or token present on this contract which + * belongs to a referrer and can be withdrawn. + * @dev address(0) is reserved to ETH balance + */ + mapping(uint256 => mapping(address => uint256)) private referrerIdToTokenToBalance; + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -370,7 +406,7 @@ contract TalentLayerEscrow is /** * @dev Only the owner of the platform ID or the owner can execute this function - * @param _token Token address ("0" for ETH) + * @param _token Token address (address(0) for ETH) * @return balance The balance of the platform or the protocol */ function getClaimableFeeBalance(address _token) external view returns (uint256 balance) { @@ -384,6 +420,15 @@ contract TalentLayerEscrow is return platformIdToTokenToBalance[platformId][_token]; } + /** + * @param _token Token address (address(0) for ETH) + * @return balance The balance of the referrer + */ + function getClaimableReferralBalance(address _token) external view returns (uint256 balance) { + uint256 referrerId = talentLayerIdContract.ids(_msgSender()); + return referrerIdToTokenToBalance[referrerId][_token]; + } + /** * @notice Called to get the details of a transaction * @dev Only the transaction sender or receiver can call this function @@ -444,7 +489,7 @@ contract TalentLayerEscrow is * @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 + * @param _originDataUri dataURI of the validated proposal. */ function createTransaction( uint256 _serviceId, @@ -465,13 +510,16 @@ contract TalentLayerEscrow is ? talentLayerPlatformIdContract.getPlatform(proposal.platformId) : originServiceCreationPlatform; + uint256 referralAmount = proposal.referrerId == 0 ? 0 : service.referralAmount; + uint256 transactionAmount = _calculateTotalWithFees( proposal.rateAmount, originServiceCreationPlatform.originServiceFeeRate, - originProposalCreationPlatform.originValidatedProposalFeeRate + originProposalCreationPlatform.originValidatedProposalFeeRate, + referralAmount ); - if (proposal.rateToken == address(0)) { + if (service.rateToken == address(0)) { require(msg.value == transactionAmount, "Non-matching funds"); } else { require(msg.value == 0, "Non-matching funds"); @@ -493,7 +541,8 @@ contract TalentLayerEscrow is id: transactionId, sender: sender, receiver: receiver, - token: proposal.rateToken, + token: service.rateToken, + totalAmount: proposal.rateAmount, amount: proposal.rateAmount, releasedAmount: 0, serviceId: _serviceId, @@ -508,15 +557,17 @@ contract TalentLayerEscrow is status: Status.NoDispute, arbitrator: originServiceCreationPlatform.arbitrator, arbitratorExtraData: originServiceCreationPlatform.arbitratorExtraData, - arbitrationFeeTimeout: originServiceCreationPlatform.arbitrationFeeTimeout + arbitrationFeeTimeout: originServiceCreationPlatform.arbitrationFeeTimeout, + referrerId: proposal.referrerId, + referralAmount: service.referralAmount }); nextTransactionId.increment(); talentLayerServiceContract.afterDeposit(_serviceId, _proposalId, transactionId); - if (proposal.rateToken != address(0)) { - IERC20Upgradeable(proposal.rateToken).safeTransferFrom(sender, address(this), transactionAmount); + if (service.rateToken != address(0)) { + IERC20Upgradeable(service.rateToken).safeTransferFrom(sender, address(this), transactionAmount); } _afterCreateTransaction(service.ownerId, proposal.ownerId, transactionId, _metaEvidence); @@ -527,7 +578,7 @@ contract TalentLayerEscrow is /** * @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 _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. */ @@ -548,7 +599,7 @@ contract TalentLayerEscrow is /** * @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 _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. */ @@ -710,7 +761,7 @@ contract TalentLayerEscrow is * @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 + * Emits a FeesClaimed event */ function claim(uint256 _platformId, address _tokenAddress) external whenNotPaused { address payable recipient; @@ -731,6 +782,28 @@ contract TalentLayerEscrow is emit FeesClaimed(_platformId, _tokenAddress, amount); } + // =========================== Referrer functions ============================== + + /** + * @notice Allows a referrer to claim its tokens & / or ETH balance. + * @param _referrerId The ID of the referrer claiming the balance. + * @param _tokenAddress The address of the Token contract (address(0) if balance in ETH). + * @dev Emits a ReferralAmountClaimed events + */ + function claimReferralBalance(uint256 _referrerId, address _tokenAddress) external whenNotPaused { + address payable recipient; + + talentLayerIdContract.isValid(_referrerId); + recipient = payable(talentLayerIdContract.ownerOf(_referrerId)); + + uint256 amount = referrerIdToTokenToBalance[_referrerId][_tokenAddress]; + require(amount > 0, "nothing to claim"); + referrerIdToTokenToBalance[_referrerId][_tokenAddress] = 0; + _safeTransferBalance(recipient, _tokenAddress, amount); + + emit ReferralAmountClaimed(_referrerId, _tokenAddress, amount); + } + // =========================== Arbitrator functions ============================== /** @@ -843,10 +916,10 @@ contract TalentLayerEscrow is // =========================== Private functions ============================== /** - * @notice Emits the events related to the creation of a transaction. + * @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 _transactionId The ID of the transaction * @param _metaEvidence The meta evidence of the transaction */ function _afterCreateTransaction( @@ -898,12 +971,21 @@ contract TalentLayerEscrow is */ function _reimburse(uint256 _transactionId, uint256 _amount) private { Transaction storage transaction = transactions[_transactionId]; - uint256 totalReleaseAmount = _calculateTotalWithFees( + + // If no referrerId (=0), the referralAmount will always be 0 + // @dev: Legacy transactions will have a totalAmount of 0; need to check for that + // @dev: This check should be removed in a future upgrade, when all legacy transactions will be completed + uint256 reimbursedReferralAmount = transaction.totalAmount != 0 + ? (_amount * transaction.referralAmount) / (transaction.totalAmount) + : 0; + + uint256 totalReimburseAmount = _calculateTotalWithFees( _amount, transaction.originServiceFeeRate, - transaction.originValidatedProposalFeeRate + transaction.originValidatedProposalFeeRate, + reimbursedReferralAmount ); - _safeTransferBalance(payable(transaction.sender), transaction.token, totalReleaseAmount); + _safeTransferBalance(payable(transaction.sender), transaction.token, totalReimburseAmount); _afterPayment(_transactionId, PaymentType.Reimburse, _amount); } @@ -934,12 +1016,25 @@ contract TalentLayerEscrow is transaction.token ] += originValidatedProposalFeeRate; + if (transaction.referrerId != 0 && transaction.referralAmount != 0) { + uint256 releasedReferralAmount = (_releaseAmount * transaction.referralAmount) / (transaction.totalAmount); + referrerIdToTokenToBalance[transaction.referrerId][transaction.token] += releasedReferralAmount; + + emit ReferralAmountReleased( + transaction.referrerId, + transaction.serviceId, + transaction.token, + releasedReferralAmount + ); + } + emit OriginServiceFeeRateReleased( originServiceCreationPlatformId, transaction.serviceId, transaction.token, originServiceFeeRate ); + emit OriginValidatedProposalFeeRateReleased( originValidatedProposalPlatformId, transaction.serviceId, @@ -949,7 +1044,7 @@ contract TalentLayerEscrow is } /** - * @notice Used to validate a realease or a reimburse payment + * @notice Used to validate a release 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 @@ -1016,18 +1111,21 @@ contract TalentLayerEscrow is * @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 + * @param _referralAmount the lump sum optional amount to be sent to the referrer, if any * @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 + uint16 _originValidatedProposalFeeRate, + uint256 _referralAmount ) private view returns (uint256 totalEscrowAmount) { - return - _amount + - (((_amount * protocolEscrowFeeRate) + - (_amount * _originServiceFeeRate) + - (_amount * _originValidatedProposalFeeRate)) / FEE_DIVIDER); + uint256 totalAmount = _amount + _referralAmount; + totalEscrowAmount = + totalAmount + + (((totalAmount * protocolEscrowFeeRate) + + (totalAmount * _originServiceFeeRate) + + (totalAmount * _originValidatedProposalFeeRate)) / FEE_DIVIDER); } // =========================== Overrides ============================== diff --git a/contracts/TalentLayerService.sol b/contracts/TalentLayerService.sol index 2252ca1e..09af1161 100644 --- a/contracts/TalentLayerService.sol +++ b/contracts/TalentLayerService.sol @@ -42,10 +42,12 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU * @notice Service information struct * @param status the current status of a service * @param ownerId the talentLayerId of the buyer - * @param acceptedProposalId the accepted proposal ID + * @param acceptedProposalId the accepted proposal ID, which is the ID of the user who submitted the proposal * @param dataUri token Id to IPFS URI mapping * @param transactionId the escrow transaction ID linked to the service * @param platformId the platform ID on which the service was created + * @param rateToken the token used for the service's payments, including the referral amount + * @param referralAmount the amount which the referrer will receive if a proposal provided it's ID as a referrer and was validated */ struct Service { Status status; @@ -54,6 +56,8 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU string dataUri; uint256 transactionId; uint256 platformId; + address rateToken; + uint256 referralAmount; } /** @@ -64,6 +68,7 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU * @param rateAmount the amount of token chosen * @param dataUri token Id to IPFS URI mapping * @param expirationDate the timeout for the proposal + * @param referrerId the id of the referrer (Zero if no referrer) */ struct Proposal { ProposalStatus status; @@ -73,6 +78,7 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU uint256 platformId; string dataUri; uint256 expirationDate; + uint256 referrerId; } /** @@ -93,8 +99,17 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU * @param ownerId the talentLayerId of the buyer * @param platformId platform ID on which the Service token was minted * @param dataUri token Id to IPFS URI mapping + * @param rateToken the token used for the service's payments, including the referral amount + * @param referralAmount the amount which the referrer will receive if a proposal provided it's ID as a referrer and was validated */ - event ServiceCreated(uint256 id, uint256 ownerId, uint256 platformId, string dataUri); + event ServiceCreated( + uint256 id, + uint256 ownerId, + uint256 platformId, + string dataUri, + address rateToken, + uint256 referralAmount + ); /** * @notice Emitted after a service is cancelled by the owner @@ -103,11 +118,12 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU event ServiceCancelled(uint256 id); /** - * @notice Emit when Cid is updated for a Service + * @notice Emit when data is updated for a Service * @param id The service ID * @param dataUri New service Data URI + * @param referralAmount New referral amount */ - event ServiceDetailedUpdated(uint256 indexed id, string dataUri); + event ServiceUpdated(uint256 id, string dataUri, uint256 referralAmount); /** * @notice Emitted after a new proposal is created @@ -115,20 +131,20 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU * @param ownerId The talentLayerId of the seller who made the proposal * @param dataUri token Id to IPFS URI mapping * @param status proposal status - * @param rateToken the token choose for the payment * @param rateAmount the amount of token chosen * @param platformId the platform ID on which the proposal was created * @param expirationDate the timeout for the proposal + * @param referrerId the id of the referrer (Zero if no referrer) */ event ProposalCreated( uint256 serviceId, uint256 ownerId, string dataUri, ProposalStatus status, - address rateToken, uint256 rateAmount, uint256 platformId, - uint256 expirationDate + uint256 expirationDate, + uint256 referrerId ); /** @@ -136,17 +152,17 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU * @param serviceId The service id * @param ownerId The talentLayerId of the seller who made the proposal * @param dataUri token Id to IPFS URI mapping - * @param rateToken the token choose for the payment * @param rateAmount the amount of token chosen * @param expirationDate the timeout for the proposal + * @param referrerId the id of the referrer (Zero if no referrer) */ event ProposalUpdated( uint256 serviceId, uint256 ownerId, string dataUri, - address rateToken, uint256 rateAmount, - uint256 expirationDate + uint256 expirationDate, + uint256 referrerId ); /** @@ -171,12 +187,12 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU uint256 public nextServiceId; /** - * @notice TalentLayerId address + * @notice TalentLayerId contract */ ITalentLayerID private tlId; /** - * @notice TalentLayer Platform ID registry contract + * @notice TalentLayerPlatformId contract */ ITalentLayerPlatformID public talentLayerPlatformIdContract; @@ -299,14 +315,18 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU * @param _platformId platform ID on which the Service token was created * @param _dataUri IPFS URI of the offchain data of the service * @param _signature optional platform signature to allow the operation + * @param _rateToken token address to be used for the service's payments + * @param _referralAmount the amount which the referrer will receive if a proposal provided it's ID as a referrer and was validated */ function createService( uint256 _profileId, uint256 _platformId, string calldata _dataUri, - bytes calldata _signature + bytes calldata _signature, + address _rateToken, + uint256 _referralAmount ) public payable onlyOwnerOrDelegate(_profileId) returns (uint256) { - _validateService(_profileId, _platformId, _dataUri, _signature); + _validateService(_profileId, _platformId, _dataUri, _signature, _rateToken); uint256 id = nextServiceId; nextServiceId++; @@ -316,48 +336,77 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU service.ownerId = _profileId; service.dataUri = _dataUri; service.platformId = _platformId; + service.rateToken = _rateToken; + service.referralAmount = _referralAmount; if (serviceNonce[_profileId] == 0 && proposalNonce[_profileId] == 0) { tlId.setHasActivity(_profileId); } serviceNonce[_profileId]++; - emit ServiceCreated(id, _profileId, _platformId, _dataUri); + emit ServiceCreated(id, _profileId, _platformId, _dataUri, _rateToken, _referralAmount); return id; } + /** + * @notice Update Service URI data + * @param _profileId The TalentLayer ID of the user, owner of the service + * @param _serviceId, Service ID to update + * @param _referralAmount, New referral amount + * @param _dataUri New IPFS URI + */ + function updateService( + uint256 _profileId, + uint256 _serviceId, + uint256 _referralAmount, + string calldata _dataUri + ) public onlyOwnerOrDelegate(_profileId) { + Service storage service = services[_serviceId]; + require(service.ownerId == _profileId, "Not the owner"); + require(service.status == Status.Opened, "status must be opened"); + require(bytes(_dataUri).length == 46, "Invalid cid"); + require(_referralAmount >= service.referralAmount, "Can't reduce referral amount"); + + service.dataUri = _dataUri; + service.referralAmount = _referralAmount; + + emit ServiceUpdated(_serviceId, _dataUri, _referralAmount); + } + /** * @notice Allows an seller to propose his service for a service * @param _profileId The TalentLayer ID of the user owner of the proposal * @param _serviceId The service linked to the new proposal - * @param _rateToken the token choose for the payment * @param _rateAmount the amount of token chosen * @param _dataUri token Id to IPFS URI mapping * @param _platformId platform ID from where the proposal is created * @param _expirationDate the time before the proposal is automatically validated * @param _signature optional platform signature to allow the operation + * @param _referrerId the id of the referrer (Zero if no referrer) */ function createProposal( uint256 _profileId, uint256 _serviceId, - address _rateToken, uint256 _rateAmount, uint256 _platformId, string calldata _dataUri, uint256 _expirationDate, - bytes calldata _signature + bytes calldata _signature, + uint256 _referrerId ) public payable onlyOwnerOrDelegate(_profileId) { - _validateProposal(_profileId, _serviceId, _rateToken, _rateAmount, _platformId, _dataUri, _signature); + Service memory service = services[_serviceId]; + _validateProposal(_profileId, _serviceId, _rateAmount, _platformId, _dataUri, _signature, _referrerId); proposals[_serviceId][_profileId] = Proposal({ status: ProposalStatus.Pending, ownerId: _profileId, - rateToken: _rateToken, + rateToken: service.rateToken, rateAmount: _rateAmount, platformId: _platformId, dataUri: _dataUri, - expirationDate: _expirationDate + expirationDate: _expirationDate, + referrerId: _referrerId }); if (serviceNonce[_profileId] == 0 && proposalNonce[_profileId] == 0) { @@ -370,10 +419,10 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU _profileId, _dataUri, ProposalStatus.Pending, - _rateToken, _rateAmount, _platformId, - _expirationDate + _expirationDate, + _referrerId ); } @@ -381,35 +430,37 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU * @notice Allows the owner to update his own proposal for a given service * @param _profileId The TalentLayer ID of the user * @param _serviceId The service linked to the new proposal - * @param _rateToken the token choose for the payment * @param _rateAmount the amount of token chosen * @param _dataUri token Id to IPFS URI mapping * @param _expirationDate the time before the proposal is automatically validated + * @param _referrerId the id of the referrer (Zero if no referrer) */ function updateProposal( uint256 _profileId, uint256 _serviceId, - address _rateToken, uint256 _rateAmount, string calldata _dataUri, - uint256 _expirationDate + uint256 _expirationDate, + uint256 _referrerId ) public onlyOwnerOrDelegate(_profileId) { - require(allowedTokenList[_rateToken].isWhitelisted, "Token not allowed"); - Service storage service = services[_serviceId]; Proposal storage proposal = proposals[_serviceId][_profileId]; require(service.status == Status.Opened, "Service not opened"); require(proposal.ownerId == _profileId, "Not the owner"); require(bytes(_dataUri).length == 46, "Invalid cid"); require(proposal.status != ProposalStatus.Validated, "Already validated"); - require(_rateAmount >= allowedTokenList[_rateToken].minimumTransactionAmount, "Amount too low"); + require(_rateAmount >= allowedTokenList[service.rateToken].minimumTransactionAmount, "Amount too low"); + + if (_referrerId != 0) { + tlId.isValid(_referrerId); + } - proposal.rateToken = _rateToken; proposal.rateAmount = _rateAmount; proposal.dataUri = _dataUri; proposal.expirationDate = _expirationDate; + proposal.referrerId = _referrerId; - emit ProposalUpdated(_serviceId, _profileId, _dataUri, _rateToken, _rateAmount, _expirationDate); + emit ProposalUpdated(_serviceId, _profileId, _dataUri, _rateAmount, _expirationDate, _referrerId); } /** @@ -470,27 +521,6 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU } } - /** - * @notice Update Service URI data - * @param _profileId The TalentLayer ID of the user, owner of the service - * @param _serviceId, Service ID to update - * @param _dataUri New IPFS URI - */ - function updateServiceData( - uint256 _profileId, - uint256 _serviceId, - string calldata _dataUri - ) public onlyOwnerOrDelegate(_profileId) { - Service storage service = services[_serviceId]; - require(service.ownerId == _profileId, "Not the owner"); - require(service.status == Status.Opened, "status must be opened"); - require(bytes(_dataUri).length == 46, "Invalid cid"); - - service.dataUri = _dataUri; - - emit ServiceDetailedUpdated(_serviceId, _dataUri); - } - /** * @notice Cancel an open Service * @param _profileId The TalentLayer ID of the user, owner of the service @@ -549,16 +579,19 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU * @param _platformId platform ID on which the Service was created * @param _dataUri token Id to IPFS URI mapping * @param _signature platform signature to allow the operation + * @param _rateToken token address to be validated */ function _validateService( uint256 _profileId, uint256 _platformId, string calldata _dataUri, - bytes calldata _signature + bytes calldata _signature, + address _rateToken ) private view { uint256 servicePostingFee = talentLayerPlatformIdContract.getServicePostingFee(_platformId); require(msg.value == servicePostingFee, "Non-matching funds"); require(bytes(_dataUri).length == 46, "Invalid cid"); + require(allowedTokenList[_rateToken].isWhitelisted, "Token not allowed"); address platformSigner = talentLayerPlatformIdContract.getSigner(_platformId); if (platformSigner != address(0)) { @@ -573,27 +606,29 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU * @notice Validate a new proposal * @param _profileId The TalentLayer ID of the user * @param _serviceId The service linked to the new proposal - * @param _rateToken the token choose for the payment - * @param _rateAmount the amount of token chosen - * @param _platformId platform ID on which the Proposal was created - * @param _dataUri token Id to IPFS URI mapping - * @param _signature platform signature to allow the operation + * @param _rateAmount The amount of token chosen + * @param _platformId Platform ID on which the Proposal was created + * @param _dataUri Token Id to IPFS URI mapping + * @param _signature Platform signature to allow the operation + * @param _referrerId The TalentLayer ID of the referrer */ function _validateProposal( uint256 _profileId, uint256 _serviceId, - address _rateToken, uint256 _rateAmount, uint256 _platformId, string calldata _dataUri, - bytes calldata _signature + bytes calldata _signature, + uint256 _referrerId ) private view { - require(allowedTokenList[_rateToken].isWhitelisted, "Token not allowed"); uint256 proposalPostingFee = talentLayerPlatformIdContract.getProposalPostingFee(_platformId); require(msg.value == proposalPostingFee, "Non-matching funds"); - require(_rateAmount >= allowedTokenList[_rateToken].minimumTransactionAmount, "Amount too low"); - Service storage service = services[_serviceId]; + Service memory service = services[_serviceId]; + require( + _rateAmount + service.referralAmount >= allowedTokenList[service.rateToken].minimumTransactionAmount, + "Amount too low" + ); require(service.status == Status.Opened, "Service not opened"); require(service.ownerId != 0, "Service not exist"); require(proposals[_serviceId][_profileId].ownerId != _profileId, "proposal already exist"); @@ -606,6 +641,10 @@ contract TalentLayerService is Initializable, ERC2771RecipientUpgradeable, UUPSU bytes32 messageHash = keccak256(abi.encodePacked("createProposal", _profileId, ";", _serviceId, _dataUri)); _validatePlatformSignature(_signature, messageHash, platformSigner); } + + if (_referrerId != 0) { + tlId.isValid(_referrerId); + } } /** diff --git a/contracts/archive/TalentLayerEscrowV1.sol b/contracts/archive/TalentLayerEscrowV1.sol index fdf3faf4..f6d31699 100644 --- a/contracts/archive/TalentLayerEscrowV1.sol +++ b/contracts/archive/TalentLayerEscrowV1.sol @@ -7,7 +7,7 @@ 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 {ITalentLayerServiceV1} from "./interfaces/ITalentLayerServiceV1.sol"; import {ITalentLayerID} from "../interfaces/ITalentLayerID.sol"; import {ITalentLayerPlatformID} from "../interfaces/ITalentLayerPlatformID.sol"; import "../libs/ERC2771RecipientUpgradeable.sol"; @@ -263,7 +263,7 @@ contract TalentLayerEscrowV1 is /** * @notice Instance of TalentLayerService.sol */ - ITalentLayerService private talentLayerServiceContract; + ITalentLayerServiceV1 private talentLayerServiceContract; /** * @notice Instance of TalentLayerID.sol @@ -354,7 +354,7 @@ contract TalentLayerEscrowV1 is __Ownable_init(); __UUPSUpgradeable_init(); - talentLayerServiceContract = ITalentLayerService(_talentLayerServiceAddress); + talentLayerServiceContract = ITalentLayerServiceV1(_talentLayerServiceAddress); talentLayerIdContract = ITalentLayerID(_talentLayerIDAddress); talentLayerPlatformIdContract = ITalentLayerPlatformID(_talentLayerPlatformIDAddress); protocolWallet = payable(_protocolWallet); @@ -451,8 +451,8 @@ contract TalentLayerEscrowV1 is string memory _originDataUri ) external payable whenNotPaused returns (uint256) { ( - ITalentLayerService.Service memory service, - ITalentLayerService.Proposal memory proposal + ITalentLayerServiceV1.Service memory service, + ITalentLayerServiceV1.Proposal memory proposal ) = talentLayerServiceContract.getServiceAndProposal(_serviceId, _proposalId); (address sender, address receiver) = talentLayerIdContract.ownersOf(service.ownerId, proposal.ownerId); @@ -478,8 +478,8 @@ contract TalentLayerEscrowV1 is 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(service.status == ITalentLayerServiceV1.Status.Opened, "Service status not open"); + require(proposal.status == ITalentLayerServiceV1.ProposalStatus.Pending, "Proposal status not pending"); require(bytes(_metaEvidence).length == 46, "Invalid cid"); require( keccak256(abi.encodePacked(proposal.dataUri)) == keccak256(abi.encodePacked(_originDataUri)), @@ -914,8 +914,8 @@ contract TalentLayerEscrowV1 is function _distributeFees(uint256 _transactionId, uint256 _releaseAmount) private { Transaction storage transaction = transactions[_transactionId]; ( - ITalentLayerService.Service memory service, - ITalentLayerService.Proposal memory proposal + ITalentLayerServiceV1.Service memory service, + ITalentLayerServiceV1.Proposal memory proposal ) = talentLayerServiceContract.getServiceAndProposal(transaction.serviceId, transaction.proposalId); uint256 originServiceCreationPlatformId = service.platformId; diff --git a/contracts/archive/TalentLayerServiceV1.sol b/contracts/archive/TalentLayerServiceV1.sol new file mode 100644 index 00000000..4ad632be --- /dev/null +++ b/contracts/archive/TalentLayerServiceV1.sol @@ -0,0 +1,635 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import {ITalentLayerID} from "../interfaces/ITalentLayerID.sol"; +import {ITalentLayerPlatformID} from "../interfaces/ITalentLayerPlatformID.sol"; +import {ERC2771RecipientUpgradeable} from "../libs/ERC2771RecipientUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ECDSAUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; + +/** + * @title TalentLayerService Contract + * @author TalentLayer Team | Website: https://talentlayer.org | Twitter: @talentlayer + */ +contract TalentLayerServiceV1 is Initializable, ERC2771RecipientUpgradeable, UUPSUpgradeable, AccessControlUpgradeable { + // =========================== Enum ============================== + + /** + * @notice Enum service status + */ + enum Status { + Opened, + Confirmed, + Finished, + Cancelled, + Uncompleted + } + + /** + * @notice Enum proposal status + */ + enum ProposalStatus { + Pending, + Validated + } + + // =========================== Struct ============================== + + /** + * @notice Service information struct + * @param status the current status of a service + * @param ownerId the talentLayerId of the buyer + * @param acceptedProposalId the accepted proposal ID + * @param dataUri token Id to IPFS URI mapping + * @param transactionId the escrow transaction ID linked to the service + * @param platformId the platform ID on which the service was created + */ + struct Service { + Status status; + uint256 ownerId; + uint256 acceptedProposalId; + string dataUri; + uint256 transactionId; + uint256 platformId; + } + + /** + * @notice Proposal information struct + * @param status the current status of a service + * @param ownerId the talentLayerId of the seller + * @param rateToken the token choose for the payment + * @param rateAmount the amount of token chosen + * @param dataUri token Id to IPFS URI mapping + * @param expirationDate the timeout for the proposal + */ + struct Proposal { + ProposalStatus status; + uint256 ownerId; + address rateToken; + uint256 rateAmount; + uint256 platformId; + string dataUri; + uint256 expirationDate; + } + + /** + * @notice Whitelisted token information struct + * @param tokenAddress the token address + * @param minimumTransactionAmount the minimum transaction value + */ + struct AllowedToken { + bool isWhitelisted; + uint256 minimumTransactionAmount; + } + + // =========================== Events ============================== + + /** + * @notice Emitted after a new service is created + * @param id The service ID (incremental) + * @param ownerId the talentLayerId of the buyer + * @param platformId platform ID on which the Service token was minted + * @param dataUri token Id to IPFS URI mapping + */ + event ServiceCreated(uint256 id, uint256 ownerId, uint256 platformId, string dataUri); + + /** + * @notice Emitted after a service is cancelled by the owner + * @param id The service ID + */ + event ServiceCancelled(uint256 id); + + /** + * @notice Emit when Cid is updated for a Service + * @param id The service ID + * @param dataUri New service Data URI + */ + event ServiceDetailedUpdated(uint256 indexed id, string dataUri); + + /** + * @notice Emitted after a new proposal is created + * @param serviceId The service id + * @param ownerId The talentLayerId of the seller who made the proposal + * @param dataUri token Id to IPFS URI mapping + * @param status proposal status + * @param rateToken the token choose for the payment + * @param rateAmount the amount of token chosen + * @param platformId the platform ID on which the proposal was created + * @param expirationDate the timeout for the proposal + */ + event ProposalCreated( + uint256 serviceId, + uint256 ownerId, + string dataUri, + ProposalStatus status, + address rateToken, + uint256 rateAmount, + uint256 platformId, + uint256 expirationDate + ); + + /** + * @notice Emitted after an existing proposal has been updated + * @param serviceId The service id + * @param ownerId The talentLayerId of the seller who made the proposal + * @param dataUri token Id to IPFS URI mapping + * @param rateToken the token choose for the payment + * @param rateAmount the amount of token chosen + * @param expirationDate the timeout for the proposal + */ + event ProposalUpdated( + uint256 serviceId, + uint256 ownerId, + string dataUri, + address rateToken, + uint256 rateAmount, + uint256 expirationDate + ); + + /** + * @notice Emitted when the contract owner adds or removes a token from the allowed payment tokens list + * @param tokenAddress The address of the payment token + * @param isWhitelisted Whether the token is allowed or not + * @param minimumTransactionAmount The minimum transaction fees for the token + */ + event AllowedTokenListUpdated(address tokenAddress, bool isWhitelisted, uint256 minimumTransactionAmount); + + /** + * @notice Emitted when the contract owner updates the minimum completion percentage for services + * @param minCompletionPercentage The new minimum completion percentage + */ + event MinCompletionPercentageUpdated(uint256 minCompletionPercentage); + + // =========================== Mappings & Variables ============================== + + /** + * @notice incremental service Id + */ + uint256 public nextServiceId; + + /** + * @notice TalentLayerId address + */ + ITalentLayerID private tlId; + + /** + * @notice TalentLayer Platform ID registry contract + */ + ITalentLayerPlatformID public talentLayerPlatformIdContract; + + /** + * @notice services mappings index by ID + */ + mapping(uint256 => Service) public services; + + /** + * @notice proposals mappings index by service ID and owner TID + */ + mapping(uint256 => mapping(uint256 => Proposal)) public proposals; + + /** + * @notice TLUserId mappings to number of services created used as a nonce in createService signature + */ + mapping(uint256 => uint256) public serviceNonce; + + /** + * @notice TLUserId mappings to number of proposals created + */ + mapping(uint256 => uint256) public proposalNonce; + + /** + * @notice Allowed payment tokens addresses + */ + mapping(address => AllowedToken) public allowedTokenList; + + /** + * @notice Minimum percentage of the proposal amount to be released for considering the service as completed + */ + uint256 public minCompletionPercentage; + + /** + * @notice Role granting Escrow permission + */ + bytes32 public constant ESCROW_ROLE = keccak256("ESCROW_ROLE"); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + // =========================== 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"); + _; + } + + // =========================== Initializers ============================== + + /** + * @notice First initializer function + * @param _talentLayerIdAddress TalentLayerId contract address + * @param _talentLayerPlatformIdAddress TalentLayerPlatformId contract address + */ + function initialize(address _talentLayerIdAddress, address _talentLayerPlatformIdAddress) public initializer { + __Ownable_init(); + __AccessControl_init(); + __UUPSUpgradeable_init(); + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + tlId = ITalentLayerID(_talentLayerIdAddress); + talentLayerPlatformIdContract = ITalentLayerPlatformID(_talentLayerPlatformIdAddress); + nextServiceId = 1; + updateMinCompletionPercentage(30); + } + + // =========================== View functions ============================== + + /** + * @notice Returns the whole service data information + * @param _serviceId Service identifier + */ + function getService(uint256 _serviceId) external view returns (Service memory) { + require(_serviceId < nextServiceId, "This service doesn't exist"); + return services[_serviceId]; + } + + /** + * @notice Returns the specific proposal for the attached service + * @param _serviceId Service identifier + * @param _proposalId Proposal identifier + */ + function getProposal(uint256 _serviceId, uint256 _proposalId) external view returns (Proposal memory) { + return proposals[_serviceId][_proposalId]; + } + + /** + * @notice Returns the specific service and the proposal linked to it + * @param _serviceId Service identifier + * @param _proposalId Proposal identifier + */ + function getServiceAndProposal( + uint256 _serviceId, + uint256 _proposalId + ) external view returns (Service memory, Proposal memory) { + Service memory service = services[_serviceId]; + Proposal memory proposal = proposals[_serviceId][_proposalId]; + return (service, proposal); + } + + /** + * @notice Indicates whether the token in parameter is allowed for payment + * @param _tokenAddress Token address + */ + function isTokenAllowed(address _tokenAddress) external view returns (bool) { + return allowedTokenList[_tokenAddress].isWhitelisted; + } + + // =========================== User functions ============================== + + /** + * @notice Allows a buyer to initiate an open service + * @param _profileId The TalentLayer ID of the user owner of the service + * @param _platformId platform ID on which the Service token was created + * @param _dataUri IPFS URI of the offchain data of the service + * @param _signature optional platform signature to allow the operation + */ + function createService( + uint256 _profileId, + uint256 _platformId, + string calldata _dataUri, + bytes calldata _signature + ) public payable onlyOwnerOrDelegate(_profileId) returns (uint256) { + _validateService(_profileId, _platformId, _dataUri, _signature); + + uint256 id = nextServiceId; + nextServiceId++; + + Service storage service = services[id]; + service.status = Status.Opened; + service.ownerId = _profileId; + service.dataUri = _dataUri; + service.platformId = _platformId; + + if (serviceNonce[_profileId] == 0 && proposalNonce[_profileId] == 0) { + tlId.setHasActivity(_profileId); + } + serviceNonce[_profileId]++; + + emit ServiceCreated(id, _profileId, _platformId, _dataUri); + + return id; + } + + /** + * @notice Allows an seller to propose his service for a service + * @param _profileId The TalentLayer ID of the user owner of the proposal + * @param _serviceId The service linked to the new proposal + * @param _rateToken the token choose for the payment + * @param _rateAmount the amount of token chosen + * @param _dataUri token Id to IPFS URI mapping + * @param _platformId platform ID from where the proposal is created + * @param _expirationDate the time before the proposal is automatically validated + * @param _signature optional platform signature to allow the operation + */ + function createProposal( + uint256 _profileId, + uint256 _serviceId, + address _rateToken, + uint256 _rateAmount, + uint256 _platformId, + string calldata _dataUri, + uint256 _expirationDate, + bytes calldata _signature + ) public payable onlyOwnerOrDelegate(_profileId) { + _validateProposal(_profileId, _serviceId, _rateToken, _rateAmount, _platformId, _dataUri, _signature); + + proposals[_serviceId][_profileId] = Proposal({ + status: ProposalStatus.Pending, + ownerId: _profileId, + rateToken: _rateToken, + rateAmount: _rateAmount, + platformId: _platformId, + dataUri: _dataUri, + expirationDate: _expirationDate + }); + + if (serviceNonce[_profileId] == 0 && proposalNonce[_profileId] == 0) { + tlId.setHasActivity(_profileId); + } + proposalNonce[_profileId]++; + + emit ProposalCreated( + _serviceId, + _profileId, + _dataUri, + ProposalStatus.Pending, + _rateToken, + _rateAmount, + _platformId, + _expirationDate + ); + } + + /** + * @notice Allows the owner to update his own proposal for a given service + * @param _profileId The TalentLayer ID of the user + * @param _serviceId The service linked to the new proposal + * @param _rateToken the token choose for the payment + * @param _rateAmount the amount of token chosen + * @param _dataUri token Id to IPFS URI mapping + * @param _expirationDate the time before the proposal is automatically validated + */ + function updateProposal( + uint256 _profileId, + uint256 _serviceId, + address _rateToken, + uint256 _rateAmount, + string calldata _dataUri, + uint256 _expirationDate + ) public onlyOwnerOrDelegate(_profileId) { + require(allowedTokenList[_rateToken].isWhitelisted, "Token not allowed"); + + Service storage service = services[_serviceId]; + Proposal storage proposal = proposals[_serviceId][_profileId]; + require(service.status == Status.Opened, "Service not opened"); + require(proposal.ownerId == _profileId, "Not the owner"); + require(bytes(_dataUri).length == 46, "Invalid cid"); + require(proposal.status != ProposalStatus.Validated, "Already validated"); + require(_rateAmount >= allowedTokenList[_rateToken].minimumTransactionAmount, "Amount too low"); + + proposal.rateToken = _rateToken; + proposal.rateAmount = _rateAmount; + proposal.dataUri = _dataUri; + proposal.expirationDate = _expirationDate; + + emit ProposalUpdated(_serviceId, _profileId, _dataUri, _rateToken, _rateAmount, _expirationDate); + } + + /** + * @notice Allow the escrow contract to upgrade the Service state after a deposit has been done + * @param _serviceId Service identifier + * @param _proposalId The chosen proposal id for this service + * @param _transactionId The escrow transaction Id + */ + function afterDeposit( + uint256 _serviceId, + uint256 _proposalId, + uint256 _transactionId + ) external onlyRole(ESCROW_ROLE) { + Service storage service = services[_serviceId]; + Proposal storage proposal = proposals[_serviceId][_proposalId]; + + service.status = Status.Confirmed; + service.acceptedProposalId = proposal.ownerId; + service.transactionId = _transactionId; + proposal.status = ProposalStatus.Validated; + } + + /** + * @notice Allows the contract owner to add or remove a token from the allowed payment tokens list + * @param _tokenAddress The address of the payment token + * @param _isWhitelisted Whether the token is allowed or not + * @param _minimumTransactionAmount the minimum amount of token allowed for a transaction + * @dev Only the contract owner can call this function. The owner can't never remove the Ox address + */ + function updateAllowedTokenList( + address _tokenAddress, + bool _isWhitelisted, + uint256 _minimumTransactionAmount + ) public onlyOwner { + if (_tokenAddress == address(0) && !_isWhitelisted) { + revert("Owner can't remove Ox address"); + } + allowedTokenList[_tokenAddress].isWhitelisted = _isWhitelisted; + allowedTokenList[_tokenAddress].minimumTransactionAmount = _minimumTransactionAmount; + + emit AllowedTokenListUpdated(_tokenAddress, _isWhitelisted, _minimumTransactionAmount); + } + + /** + * @notice Allow the escrow contract to upgrade the Service state after the full payment has been received by the seller + * @param _serviceId Service identifier + * @param _releasedAmount The total amount of the payment released to the seller + */ + function afterFullPayment(uint256 _serviceId, uint256 _releasedAmount) external onlyRole(ESCROW_ROLE) { + Service storage service = services[_serviceId]; + Proposal storage proposal = proposals[_serviceId][service.acceptedProposalId]; + + uint256 releasedPercentage = (_releasedAmount * 100) / proposal.rateAmount; + if (releasedPercentage >= minCompletionPercentage) { + service.status = Status.Finished; + } else { + service.status = Status.Uncompleted; + } + } + + /** + * @notice Update Service URI data + * @param _profileId The TalentLayer ID of the user, owner of the service + * @param _serviceId, Service ID to update + * @param _dataUri New IPFS URI + */ + function updateServiceData( + uint256 _profileId, + uint256 _serviceId, + string calldata _dataUri + ) public onlyOwnerOrDelegate(_profileId) { + Service storage service = services[_serviceId]; + require(service.ownerId == _profileId, "Not the owner"); + require(service.status == Status.Opened, "status must be opened"); + require(bytes(_dataUri).length == 46, "Invalid cid"); + + service.dataUri = _dataUri; + + emit ServiceDetailedUpdated(_serviceId, _dataUri); + } + + /** + * @notice Cancel an open Service + * @param _profileId The TalentLayer ID of the user, owner of the service + * @param _serviceId Service ID to cancel + */ + function cancelService(uint256 _profileId, uint256 _serviceId) public onlyOwnerOrDelegate(_profileId) { + Service storage service = services[_serviceId]; + + require(service.ownerId == _profileId, "not the owner"); + require(service.status == Status.Opened, "status must be opened"); + service.status = Status.Cancelled; + + emit ServiceCancelled(_serviceId); + } + + // =========================== Owner functions ============================== + + /** + * @notice Allows the contract owner to update the minimum completion percentage for services + * @param _minCompletionPercentage The new completion percentage + * @dev Only the contract owner can call this function + */ + function updateMinCompletionPercentage(uint256 _minCompletionPercentage) public onlyRole(DEFAULT_ADMIN_ROLE) { + minCompletionPercentage = _minCompletionPercentage; + + emit MinCompletionPercentageUpdated(_minCompletionPercentage); + } + + // =========================== 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(); + } + + // =========================== Private functions ============================== + + /** + * @notice Validate a new service + * @param _profileId The TalentLayer ID of the user + * @param _platformId platform ID on which the Service was created + * @param _dataUri token Id to IPFS URI mapping + * @param _signature platform signature to allow the operation + */ + function _validateService( + uint256 _profileId, + uint256 _platformId, + string calldata _dataUri, + bytes calldata _signature + ) private view { + uint256 servicePostingFee = talentLayerPlatformIdContract.getServicePostingFee(_platformId); + require(msg.value == servicePostingFee, "Non-matching funds"); + require(bytes(_dataUri).length == 46, "Invalid cid"); + + address platformSigner = talentLayerPlatformIdContract.getSigner(_platformId); + if (platformSigner != address(0)) { + bytes32 messageHash = keccak256( + abi.encodePacked("createService", _profileId, ";", serviceNonce[_profileId], _dataUri) + ); + _validatePlatformSignature(_signature, messageHash, platformSigner); + } + } + + /** + * @notice Validate a new proposal + * @param _profileId The TalentLayer ID of the user + * @param _serviceId The service linked to the new proposal + * @param _rateToken the token choose for the payment + * @param _rateAmount the amount of token chosen + * @param _platformId platform ID on which the Proposal was created + * @param _dataUri token Id to IPFS URI mapping + * @param _signature platform signature to allow the operation + */ + function _validateProposal( + uint256 _profileId, + uint256 _serviceId, + address _rateToken, + uint256 _rateAmount, + uint256 _platformId, + string calldata _dataUri, + bytes calldata _signature + ) private view { + require(allowedTokenList[_rateToken].isWhitelisted, "Token not allowed"); + uint256 proposalPostingFee = talentLayerPlatformIdContract.getProposalPostingFee(_platformId); + require(msg.value == proposalPostingFee, "Non-matching funds"); + require(_rateAmount >= allowedTokenList[_rateToken].minimumTransactionAmount, "Amount too low"); + + Service storage service = services[_serviceId]; + require(service.status == Status.Opened, "Service not opened"); + require(service.ownerId != 0, "Service not exist"); + require(proposals[_serviceId][_profileId].ownerId != _profileId, "proposal already exist"); + + require(service.ownerId != _profileId, "can't create for your own service"); + require(bytes(_dataUri).length == 46, "Invalid cid"); + + address platformSigner = talentLayerPlatformIdContract.getSigner(_platformId); + if (platformSigner != address(0)) { + bytes32 messageHash = keccak256(abi.encodePacked("createProposal", _profileId, ";", _serviceId, _dataUri)); + _validatePlatformSignature(_signature, messageHash, platformSigner); + } + } + + /** + * @notice Validate the platform ECDSA signature for a given message hash operation + * @param _signature platform signature to allow the operation + * @param _messageHash The hash of a generated message corresponding to the operation + * @param _platformSigner The address defined by the platform as signer + */ + function _validatePlatformSignature( + bytes calldata _signature, + bytes32 _messageHash, + address _platformSigner + ) private pure { + bytes32 ethMessageHash = ECDSAUpgradeable.toEthSignedMessageHash(_messageHash); + address signer = ECDSAUpgradeable.recover(ethMessageHash, _signature); + require(_platformSigner == signer, "invalid signature"); + } + + // =========================== 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 onlyOwner {} +} diff --git a/contracts/archive/interfaces/ITalentLayerEscrowV1.sol b/contracts/archive/interfaces/ITalentLayerEscrowV1.sol new file mode 100644 index 00000000..d289022f --- /dev/null +++ b/contracts/archive/interfaces/ITalentLayerEscrowV1.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +/** + * @title Platform ID Interface + * @author TalentLayer Team | Website: https://talentlayer.org | Twitter: @talentlayer + */ +interface ITalentLayerEscrowV1 { + struct Transaction { + uint256 id; + address sender; + address receiver; + address token; + uint256 amount; + uint256 serviceId; + uint16 protocolEscrowFeeRate; + uint16 originServiceFeeRate; + uint16 originValidatedProposalFeeRate; + uint256 disputeId; + uint256 senderFee; + uint256 receiverFee; + uint256 lastInteraction; + Status status; + // Arbitrator arbitrator; + bytes arbitratorExtraData; + uint256 arbitrationFeeTimeout; + } + + 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) + } + + function getClaimableFeeBalance(address _token) external view returns (uint256 balance); + + function getTransactionDetails(uint256 _transactionId) external view returns (Transaction memory); + + function updateProtocolEscrowFeeRate(uint16 _protocolEscrowFeeRate) external; + + function updateProtocolWallet(address payable _protocolWallet) external; + + function createTransaction( + uint256 _serviceId, + uint256 _proposalId, + string memory _metaEvidence, + string memory originDataUri + ) external payable returns (uint256); + + function release(uint256 _transactionId, uint256 _amount) external; + + function reimburse(uint256 _transactionId, uint256 _amount) external; + + function claim(uint256 _platformId, address _tokenAddress) external; + + function claimAll(uint256 _platformId) external; + + function payArbitrationFeeBySender(uint256 _transactionId) external payable; + + function payArbitrationFeeByReceiver(uint256 _transactionId) external payable; + + function timeOutBySender(uint256 _transactionId) external; + + function timeOutByReceiver(uint256 _transactionId) external; + + function submitEvidence(uint256 _transactionId, string memory _evidence) external; + + function appeal(uint256 _transactionId) external payable; + + function rule(uint256 _disputeID, uint256 _ruling) external; +} diff --git a/contracts/archive/interfaces/ITalentLayerServiceV1.sol b/contracts/archive/interfaces/ITalentLayerServiceV1.sol new file mode 100644 index 00000000..41876fa5 --- /dev/null +++ b/contracts/archive/interfaces/ITalentLayerServiceV1.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +/** + * @title Platform ID Interface + * @author TalentLayer Team | Website: https://talentlayer.org | Twitter: @talentlayer + */ +interface ITalentLayerServiceV1 { + enum Status { + Opened, + Confirmed, + Finished, + Cancelled, + Uncompleted + } + + enum ProposalStatus { + Pending, + Validated + } + + struct Service { + Status status; + uint256 ownerId; + uint256 acceptedProposalId; + string dataUri; + uint256 transactionId; + uint256 platformId; + } + + struct Proposal { + ProposalStatus status; + uint256 ownerId; + address rateToken; + uint256 rateAmount; + uint256 platformId; + string dataUri; + uint256 expirationDate; + } + + function getService(uint256 _serviceId) external view returns (Service memory); + + function getProposal(uint256 _serviceId, uint256 _proposal) external view returns (Proposal memory); + + function getServiceAndProposal( + uint256 _serviceId, + uint256 _proposal + ) external view returns (Service memory, Proposal memory); + + function createService( + Status _status, + uint256 _tokenId, + uint256 _platformId, + uint256 _ownerId, + string calldata _dataUri + ) external returns (uint256); + + function createProposal( + uint256 _serviceId, + address _rateToken, + uint256 _rateAmount, + uint256 _platformId, + string calldata _dataUri + ) external; + + function afterDeposit(uint256 _serviceId, uint256 _proposalId, uint256 _transactionId) external; + + function updateProposal( + uint256 _serviceId, + address _rateToken, + uint256 _rateAmount, + string calldata _dataUri + ) external; + + function afterFullPayment(uint256 _serviceId, uint256 _releasedAmount) external; + + function updateServiceData(uint256 _serviceId, string calldata _dataUri) external; +} diff --git a/contracts/interfaces/ITalentLayerEscrow.sol b/contracts/interfaces/ITalentLayerEscrow.sol index b1b56744..987300ce 100644 --- a/contracts/interfaces/ITalentLayerEscrow.sol +++ b/contracts/interfaces/ITalentLayerEscrow.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; +import "../Arbitrator.sol"; + /** * @title Platform ID Interface * @author TalentLayer Team | Website: https://talentlayer.org | Twitter: @talentlayer @@ -12,18 +14,23 @@ interface ITalentLayerEscrow { 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; - Status status; - // Arbitrator arbitrator; bytes arbitratorExtraData; uint256 arbitrationFeeTimeout; + uint256 referrerId; + uint256 referralAmount; + uint256 totalAmount; } enum Status { @@ -36,6 +43,8 @@ interface ITalentLayerEscrow { function getClaimableFeeBalance(address _token) external view returns (uint256 balance); + function getClaimableReferralBalance(address _token) external view returns (uint256 balance); + function getTransactionDetails(uint256 _transactionId) external view returns (Transaction memory); function updateProtocolEscrowFeeRate(uint16 _protocolEscrowFeeRate) external; @@ -49,25 +58,23 @@ interface ITalentLayerEscrow { string memory originDataUri ) external payable returns (uint256); - function release(uint256 _transactionId, uint256 _amount) external; - - function reimburse(uint256 _transactionId, uint256 _amount) external; + function release(uint256 _profileId, uint256 _transactionId, uint256 _amount) external; - function claim(uint256 _platformId, address _tokenAddress) external; - - function claimAll(uint256 _platformId) external; + function reimburse(uint256 _profileId, uint256 _transactionId, uint256 _amount) external; function payArbitrationFeeBySender(uint256 _transactionId) external payable; function payArbitrationFeeByReceiver(uint256 _transactionId) external payable; - function timeOutBySender(uint256 _transactionId) external; - - function timeOutByReceiver(uint256 _transactionId) external; + function arbitrationFeeTimeout(uint256 _transactionId) external; - function submitEvidence(uint256 _transactionId, string memory _evidence) external; + function submitEvidence(uint256 _profileId, uint256 _transactionId, string memory _evidence) external; function appeal(uint256 _transactionId) external payable; + function claim(uint256 _platformId, address _tokenAddress) external; + + function claimReferralBalance(uint256 _referrerId, address _tokenAddress) external; + function rule(uint256 _disputeID, uint256 _ruling) external; } diff --git a/contracts/interfaces/ITalentLayerService.sol b/contracts/interfaces/ITalentLayerService.sol index c5359bc0..dea12dce 100644 --- a/contracts/interfaces/ITalentLayerService.sol +++ b/contracts/interfaces/ITalentLayerService.sol @@ -26,6 +26,8 @@ interface ITalentLayerService { string dataUri; uint256 transactionId; uint256 platformId; + address rateToken; + uint256 referralAmount; } struct Proposal { @@ -36,6 +38,7 @@ interface ITalentLayerService { uint256 platformId; string dataUri; uint256 expirationDate; + uint256 referrerId; } function getService(uint256 _serviceId) external view returns (Service memory); @@ -47,32 +50,55 @@ interface ITalentLayerService { uint256 _proposal ) external view returns (Service memory, Proposal memory); + function isTokenAllowed(address _tokenAddress) external view returns (bool); + function createService( - Status _status, - uint256 _tokenId, + uint256 _profileId, uint256 _platformId, - uint256 _ownerId, - string calldata _dataUri + string calldata _dataUri, + bytes calldata _signature, + address _rateToken, + uint256 _referralAmount ) external returns (uint256); + function updateService( + uint256 _profileId, + uint256 _serviceId, + uint256 _referralAmount, + string calldata _dataUri + ) external; + function createProposal( + uint256 _profileId, uint256 _serviceId, - address _rateToken, uint256 _rateAmount, uint256 _platformId, - string calldata _dataUri + string calldata _dataUri, + uint256 _expirationDate, + bytes calldata _signature, + uint256 _referrerId ) external; - function afterDeposit(uint256 _serviceId, uint256 _proposalId, uint256 _transactionId) external; - function updateProposal( + uint256 _profileId, uint256 _serviceId, - address _rateToken, uint256 _rateAmount, - string calldata _dataUri + string calldata _dataUri, + uint256 _expirationDate, + uint256 _referrerId + ) external; + + function afterDeposit(uint256 _serviceId, uint256 _proposalId, uint256 _transactionId) external; + + function updateAllowedTokenList( + address _tokenAddress, + bool _isWhitelisted, + uint256 _minimumTransactionAmount ) external; function afterFullPayment(uint256 _serviceId, uint256 _releasedAmount) external; - function updateServiceData(uint256 _serviceId, string calldata _dataUri) external; + function cancelService(uint256 _profileId, uint256 _serviceId) external; + + function updateMinCompletionPercentage(uint256 _minCompletionPercentage) external; } diff --git a/scripts/playground/2-create-service.ts b/scripts/playground/2-create-service.ts index b0829393..36077519 100644 --- a/scripts/playground/2-create-service.ts +++ b/scripts/playground/2-create-service.ts @@ -5,6 +5,7 @@ import { getSignatureForService } from '../../test/utils/signature' import hre = require('hardhat') const aliceTlId = 1 +const referralAmount = 0 /* In this script Alice will create two services. @@ -56,7 +57,7 @@ async function main() { const createFirstOpenService = await talentLayerService .connect(alice) - .createService(aliceTlId, daveTalentLayerIdPlatform, aliceCreateFirstJobData, signatureFirstJob) + .createService(aliceTlId, daveTalentLayerIdPlatform, aliceCreateFirstJobData, signatureFirstJob, ethers.constants.AddressZero, referralAmount) await createFirstOpenService.wait() console.log('First Open Service created') @@ -91,6 +92,8 @@ async function main() { daveTalentLayerIdPlatform, aliceCreateSecondJobData, signatureSecondJob, + ethers.constants.AddressZero, + referralAmount, ) await createSecondOpenService.wait() console.log('Open Service 2 created') diff --git a/scripts/playground/2-update-service.ts b/scripts/playground/2-update-service.ts index 4470d5cb..8bc1912c 100644 --- a/scripts/playground/2-update-service.ts +++ b/scripts/playground/2-update-service.ts @@ -4,6 +4,7 @@ import postToIPFS from '../utils/ipfs' import hre = require('hardhat') const aliceTlId = 1 +const referralAmount = 0 /* In this script Alice will update the first service. @@ -49,7 +50,7 @@ async function main() { await talentLayerService .connect(alice) - .updateServiceData(aliceTlId, firstServiceId, aliceUpdateJobData) + .updateService(aliceTlId, firstServiceId, referralAmount, aliceUpdateJobData) const jobDataAfterUpdate = await talentLayerService.getService(firstServiceId) console.log('Job Data after update', jobDataAfterUpdate) } diff --git a/scripts/playground/3-make-proposal.ts b/scripts/playground/3-make-proposal.ts index 4417c69b..b2a5a00f 100644 --- a/scripts/playground/3-make-proposal.ts +++ b/scripts/playground/3-make-proposal.ts @@ -7,6 +7,7 @@ const aliceTlId = 1 const bobTlId = 2 const carolTlId = 3 const daveTlId = 4 +const referrerTlId = 0 const minTokenWhitelistTransactionAmount = ethers.utils.parseUnits('0.0001', 18) @@ -101,12 +102,12 @@ async function main() { .createProposal( bobTlId, firstServiceId, - rateTokenBob, ethers.utils.parseUnits('0.001', 18), davePlatformId, bobUri, proposalExpirationDate, signatureBobProposal, + referrerTlId, ) console.log('Bob proposal created') await bobProposal.wait() @@ -124,12 +125,12 @@ async function main() { .createProposal( carolTlId, firstServiceId, - rateTokenCarol, ethers.utils.parseUnits('0.002', 18), bobPlatformId, carolUri, proposalExpirationDate, signatureCarolProposal, + referrerTlId, ) console.log('Carol proposal created') await carolProposal.wait() @@ -147,12 +148,12 @@ async function main() { .createProposal( daveTlId, secondServiceId, - rateTokenDave, ethers.utils.parseUnits('0.003', 18), bobPlatformId, daveUri, proposalExpirationDate, signatureDaveProposal, + referrerTlId, ) console.log('Dave proposal created') await daveProposal.wait() diff --git a/scripts/playground/4-update-proposal.ts b/scripts/playground/4-update-proposal.ts index 26a83c5d..2e0c514c 100644 --- a/scripts/playground/4-update-proposal.ts +++ b/scripts/playground/4-update-proposal.ts @@ -4,6 +4,7 @@ import postToIPFS from '../utils/ipfs' import hre = require('hardhat') const bobTlId = 2 +const referrerTlId = 0 const now = Math.floor(Date.now() / 1000) const proposalExpirationDate = now + 60 * 60 * 24 * 30 @@ -41,10 +42,10 @@ async function main() { .updateProposal( bobTlId, firstServiceId, - rateTokenBob, ethers.utils.parseUnits('0.0015', 18), bobUri, proposalExpirationDate, + referrerTlId, ) console.log('Bob update his proposal') diff --git a/scripts/playground/8-reviews.ts b/scripts/playground/8-reviews.ts index 2336544d..d0882b2b 100644 --- a/scripts/playground/8-reviews.ts +++ b/scripts/playground/8-reviews.ts @@ -18,7 +18,7 @@ async function main() { const talentLayerReview = await ethers.getContractAt( 'TalentLayerReview', - getDeploymentProperty(network, DeploymentProperty.Reviewscontract), + getDeploymentProperty(network, DeploymentProperty.TalentLayerReview), ) const aliceReviewCarol = await postToIPFS( diff --git a/scripts/tasks/protocol/add-trusted-forwarder.ts b/scripts/tasks/protocol/add-trusted-forwarder.ts index 56b1c4f0..2db382e6 100644 --- a/scripts/tasks/protocol/add-trusted-forwarder.ts +++ b/scripts/tasks/protocol/add-trusted-forwarder.ts @@ -27,7 +27,7 @@ task('add-trusted-forwarder', 'Adds a trusted forwarder for meta transactions.') const talentLayerReview = await ethers.getContractAt( 'TalentLayerReview', - getDeploymentProperty(network.name, DeploymentProperty.Reviewscontract), + getDeploymentProperty(network.name, DeploymentProperty.TalentLayerReview), ) const talentLayerEscrow = await ethers.getContractAt( diff --git a/scripts/tasks/protocol/remove-trusted-forwarder.ts b/scripts/tasks/protocol/remove-trusted-forwarder.ts index e52cf5c1..c04a5fae 100644 --- a/scripts/tasks/protocol/remove-trusted-forwarder.ts +++ b/scripts/tasks/protocol/remove-trusted-forwarder.ts @@ -26,7 +26,7 @@ task('remove-trusted-forwarder', 'Removes a trusted forwarder for meta transacti const talentLayerReview = await ethers.getContractAt( 'TalentLayerReview', - getDeploymentProperty(network.name, DeploymentProperty.Reviewscontract), + getDeploymentProperty(network.name, DeploymentProperty.TalentLayerReview), ) const talentLayerEscrow = await ethers.getContractAt( diff --git a/test/batch/delegationSystem.ts b/test/batch/delegationSystem.ts index 67cf868f..4d988ab9 100644 --- a/test/batch/delegationSystem.ts +++ b/test/batch/delegationSystem.ts @@ -25,6 +25,7 @@ const serviceId = 1 const trasactionId = 1 const transactionAmount = 100000 const ethAddress = '0x0000000000000000000000000000000000000000' +const referralAmount = 0 /** * Deploys contracts and sets up the context for TalentLayerId contract. @@ -131,19 +132,19 @@ describe('Delegation System', function () { const signature = await getSignatureForService(platformOneOwner, aliceTlId, 0, cid) const tx = talentLayerService .connect(eve) - .createService(aliceTlId, carolPlatformId, cid, signature) + .createService(aliceTlId, carolPlatformId, cid, signature, ethers.constants.AddressZero, 0) await expect(tx).to.be.revertedWith('Not owner or delegate') await talentLayerService .connect(dave) - .createService(aliceTlId, carolPlatformId, cid, signature) + .createService(aliceTlId, carolPlatformId, cid, signature, ethers.constants.AddressZero, 0) const serviceData = await talentLayerService.services(1) expect(serviceData.ownerId.toNumber()).to.be.equal(aliceTlId) }) it('Dave can update service data on behalf of Alice', async function () { - const tx = await talentLayerService.connect(dave).updateServiceData(aliceTlId, serviceId, cid) + const tx = await talentLayerService.connect(dave).updateService(aliceTlId, serviceId, referralAmount, cid) await expect(tx).to.not.be.reverted }) @@ -154,12 +155,12 @@ describe('Delegation System', function () { .createProposal( bobTlId, serviceId, - ethAddress, transactionAmount, carolPlatformId, cid, proposalExpirationDate, signature, + 0, ) const proposal = await talentLayerService.proposals(serviceId, bobTlId) expect(proposal.ownerId).to.eq(bobTlId) @@ -171,10 +172,10 @@ describe('Delegation System', function () { .updateProposal( bobTlId, serviceId, - ethAddress, transactionAmount, cid, proposalExpirationDate, + aliceTlId, ) await expect(tx).to.not.be.reverted }) @@ -239,7 +240,7 @@ describe('Delegation System', function () { const signature = await getSignatureForService(platformOneOwner, aliceTlId, 1, cid) const tx = talentLayerService .connect(dave) - .createService(aliceTlId, carolPlatformId, cid, signature) + .createService(aliceTlId, carolPlatformId, cid, signature, ethers.constants.AddressZero, 0) await expect(tx).to.be.revertedWith('Not owner or delegate') }) }) diff --git a/test/batch/disputeResolution.ts b/test/batch/disputeResolution.ts index 62a8e674..c5d2510b 100644 --- a/test/batch/disputeResolution.ts +++ b/test/batch/disputeResolution.ts @@ -56,7 +56,7 @@ async function deployAndSetup( talentLayerEscrow, talentLayerArbitrator, talentLayerService, - ] = await deploy(true) + ] = await deploy(false) // Deployer whitelists a list of authorized tokens await talentLayerService @@ -105,7 +105,9 @@ async function deployAndSetup( // Alice, the buyer, initiates a new open service const signature = await getSignatureForService(carol, aliceTlId, 0, cid) - await talentLayerService.connect(alice).createService(aliceTlId, carolPlatformId, cid, signature) + await talentLayerService + .connect(alice) + .createService(aliceTlId, carolPlatformId, cid, signature, tokenAddress, 0) // Bob, the seller, creates a proposal for the service const signature2 = await getSignatureForProposal(carol, bobTlId, serviceId, cid) @@ -114,12 +116,12 @@ async function deployAndSetup( .createProposal( bobTlId, serviceId, - tokenAddress, transactionAmount, carolPlatformId, cid, proposalExpirationDate, signature2, + 0, ) return [talentLayerPlatformID, talentLayerEscrow, talentLayerArbitrator, talentLayerService] @@ -152,6 +154,7 @@ async function createTransaction( talentLayerEscrow: TalentLayerEscrow, talentLayerService: TalentLayerService, signer: SignerWithAddress, + tokenAddress: string, ): Promise<[ContractTransaction, BigNumber, number, number, number]> { // Create transaction const [ @@ -165,11 +168,16 @@ async function createTransaction( const proposal = await talentLayerService.proposals(serviceId, bobTlId) const value = proposal.rateToken === ethAddress ? totalTransactionAmount : 0 - const tx = await talentLayerEscrow - .connect(signer) - .createTransaction(serviceId, proposalId, metaEvidenceCid, proposal.dataUri, { - value, - }) + let tx: ContractTransaction + tokenAddress === ethAddress + ? (tx = await talentLayerEscrow + .connect(signer) + .createTransaction(serviceId, proposalId, metaEvidenceCid, proposal.dataUri, { + value, + })) + : (tx = await talentLayerEscrow + .connect(signer) + .createTransaction(serviceId, proposalId, metaEvidenceCid, proposal.dataUri)) return [ tx, @@ -229,6 +237,7 @@ describe('Dispute Resolution, standard flow', function () { talentLayerEscrow, talentLayerService, alice, + ethAddress, ) }) @@ -635,7 +644,13 @@ describe('Dispute Resolution, with sender failing to pay arbitration fee on time await deployAndSetup(arbitrationFeeTimeout, ethers.constants.AddressZero) // Create transaction - await createTransaction(talentLayerPlatformID, talentLayerEscrow, talentLayerService, alice) + await createTransaction( + talentLayerPlatformID, + talentLayerEscrow, + talentLayerService, + alice, + ethAddress, + ) // Alice wants to raise a dispute and pays the arbitration fee await talentLayerEscrow.connect(alice).payArbitrationFeeBySender(transactionId, { @@ -707,6 +722,7 @@ describe('Dispute Resolution, with receiver failing to pay arbitration fee on ti talentLayerEscrow, talentLayerService, alice, + ethAddress, ) // Bob wants to raise a dispute and pays the arbitration fee @@ -771,7 +787,13 @@ describe('Dispute Resolution, arbitrator abstaining from giving a ruling', funct // Create transaction ;[, , protocolEscrowFeeRate, originServiceFeeRate, originValidatedProposalFeeRate] = - await createTransaction(talentLayerPlatformID, talentLayerEscrow, talentLayerService, alice) + await createTransaction( + talentLayerPlatformID, + talentLayerEscrow, + talentLayerService, + alice, + ethAddress, + ) // Alice wants to raise a dispute and pays the arbitration fee await talentLayerEscrow.connect(alice).payArbitrationFeeBySender(transactionId, { @@ -877,7 +899,13 @@ describe('Dispute Resolution, receiver winning', function () { await deployAndSetup(arbitrationFeeTimeout, ethers.constants.AddressZero) // Create transaction - await createTransaction(talentLayerPlatformID, talentLayerEscrow, talentLayerService, alice) + await createTransaction( + talentLayerPlatformID, + talentLayerEscrow, + talentLayerService, + alice, + ethAddress, + ) // Bob wants to raise a dispute and pays the arbitration fee and a dispute is created await talentLayerEscrow.connect(bob).payArbitrationFeeByReceiver(transactionId, { @@ -989,7 +1017,13 @@ describe('Dispute Resolution, with ERC20 token transaction', function () { await simpleERC20.connect(alice).approve(talentLayerEscrow.address, totalTransactionAmount) // Create transaction - await createTransaction(talentLayerPlatformID, talentLayerEscrow, talentLayerService, alice) + await createTransaction( + talentLayerPlatformID, + talentLayerEscrow, + talentLayerService, + alice, + simpleERC20.address, + ) // Alice wants to raise a dispute and pays the arbitration fee await talentLayerEscrow.connect(alice).payArbitrationFeeBySender(transactionId, { diff --git a/test/batch/fullWorkflow.ts b/test/batch/fullWorkflow.ts index 87921140..713da902 100644 --- a/test/batch/fullWorkflow.ts +++ b/test/batch/fullWorkflow.ts @@ -33,6 +33,8 @@ const carolTlId = 3 const alicePlatformId = 1 const bobPlatformId = 2 +const FEE_DIVIDER = 10000 + describe('TalentLayer protocol global testing', function () { // we define the types of the variables we will use let deployer: SignerWithAddress, @@ -58,8 +60,8 @@ describe('TalentLayer protocol global testing', function () { networkConfig: NetworkConfig, chainId: number - const nonListedRateToken = '0x6b175474e89094c44da98b954eedeac495271d0f' - const rateToken = '0xC01FcDfDE3B2ABA1eab76731493C617FfAED2F10' + const nonListedtoken = '0x6b175474e89094c44da98b954eedeac495271d0f' + const referralAmount = 20000 before(async function () { // Get the Signers @@ -752,14 +754,18 @@ describe('TalentLayer protocol global testing', function () { it("Dave, who doesn't have TalentLayerID, can't create a service", async function () { const signature = await getSignatureForService(platformOneOwner, 0, 0, 'haveNotTlid') await expect( - talentLayerService.connect(dave).createService(0, 1, 'haveNotTlid', signature), + talentLayerService + .connect(dave) + .createService(0, 1, 'haveNotTlid', signature, token.address, 0), ).to.be.revertedWith('ERC721: invalid token ID') }) it("Alice can't create a new service with a talentLayerPlatformId 0", async function () { const signature = await getSignatureForService(platformOneOwner, 0, 0, cid) await expect( - talentLayerService.connect(alice).createService(aliceTlId, 0, cid, signature), + talentLayerService + .connect(alice) + .createService(aliceTlId, 0, cid, signature, token.address, 0), ).to.be.revertedWith('Invalid platform ID') }) @@ -770,31 +776,45 @@ describe('TalentLayer protocol global testing', function () { const signature = await getSignatureForService(platformOneOwner, aliceTlId, 0, cid) const tx = talentLayerService .connect(alice) - .createService(aliceTlId, alicePlatformId, cid, signature, { + .createService(aliceTlId, alicePlatformId, cid, signature, token.address, 0, { value: alicePlatformServicePostingFee.sub(1), }) await expect(tx).to.be.revertedWith('Non-matching funds') }) - it('Alice the buyer can create a few Open service', async function () { + it('Should revert if Carol tries to create a service with a non-whitelisted payment token', async function () { + const platform = await talentLayerPlatformID.getPlatform(alicePlatformId) + const alicePlatformServicePostingFee = platform.servicePostingFee + + const signature = await getSignatureForService(platformOneOwner, aliceTlId, 0, cid) + const tx = talentLayerService + .connect(alice) + .createService(aliceTlId, alicePlatformId, cid, signature, nonListedtoken, 0, { + value: alicePlatformServicePostingFee, + }) + + await expect(tx).to.be.revertedWith('Token not allowed') + }) + + it('Alice the buyer can create a few Open services', async function () { const platform = await talentLayerPlatformID.getPlatform(alicePlatformId) const alicePlatformServicePostingFee = platform.servicePostingFee // Alice will create 4 Open services fo the whole unit test process const signature = await getSignatureForService(platformOneOwner, aliceTlId, 0, cid) - await talentLayerService + const tx = await talentLayerService .connect(alice) - .createService(aliceTlId, alicePlatformId, cid, signature, { + .createService(aliceTlId, alicePlatformId, cid, signature, token.address, 0, { value: alicePlatformServicePostingFee, }) - const serviceData = await talentLayerService.services(1) + const service1Data = await talentLayerService.services(1) // service 2 const signature2 = await getSignatureForService(platformOneOwner, aliceTlId, 1, cid) await talentLayerService .connect(alice) - .createService(aliceTlId, alicePlatformId, cid, signature2, { + .createService(aliceTlId, alicePlatformId, cid, signature2, token.address, 0, { value: alicePlatformServicePostingFee, }) await talentLayerService.services(2) @@ -803,16 +823,24 @@ describe('TalentLayer protocol global testing', function () { const signature3 = await getSignatureForService(platformOneOwner, aliceTlId, 2, cid) await talentLayerService .connect(alice) - .createService(aliceTlId, alicePlatformId, cid, signature3, { - value: alicePlatformServicePostingFee, - }) + .createService( + aliceTlId, + alicePlatformId, + cid, + signature3, + ethers.constants.AddressZero, + 0, + { + value: alicePlatformServicePostingFee, + }, + ) await talentLayerService.services(3) // service 4 const signature4 = await getSignatureForService(platformOneOwner, aliceTlId, 3, cid) await talentLayerService .connect(alice) - .createService(aliceTlId, alicePlatformId, cid, signature4, { + .createService(aliceTlId, alicePlatformId, cid, signature4, token.address, 0, { value: alicePlatformServicePostingFee, }) await talentLayerService.services(4) @@ -821,28 +849,108 @@ describe('TalentLayer protocol global testing', function () { const signature5 = await getSignatureForService(platformOneOwner, aliceTlId, 4, cid) await talentLayerService .connect(alice) - .createService(aliceTlId, alicePlatformId, cid, signature5, { + .createService(aliceTlId, alicePlatformId, cid, signature5, token.address, 0, { value: alicePlatformServicePostingFee, }) await talentLayerService.services(5) - expect(serviceData.status).to.be.equal(ServiceStatus.Opened) - expect(serviceData.ownerId).to.be.equal(aliceTlId) - expect(serviceData.dataUri).to.be.equal(cid) - expect(serviceData.platformId).to.be.equal(1) + expect(service1Data.status).to.be.equal(ServiceStatus.Opened) + expect(service1Data.ownerId).to.be.equal(aliceTlId) + expect(service1Data.dataUri).to.be.equal(cid) + expect(service1Data.platformId).to.be.equal(1) + expect(service1Data.referralAmount).to.be.equal(0) + expect(service1Data.rateToken).to.be.equal(token.address) + expect(tx) + .to.emit(talentLayerService, 'ServiceCreated') + .withArgs(5, aliceTlId, alicePlatformId, cid, token.address, 0) + }) + + it('Alice the buyer can create a service using ETH with referral', async function () { + const platform = await talentLayerPlatformID.getPlatform(alicePlatformId) + const alicePlatformServicePostingFee = platform.servicePostingFee + + const signature = await getSignatureForService(platformOneOwner, aliceTlId, 5, cid) + const tx = await talentLayerService + .connect(alice) + .createService( + aliceTlId, + alicePlatformId, + cid, + signature, + ethers.constants.AddressZero, + referralAmount, + { + value: alicePlatformServicePostingFee, + }, + ) + const service6Data = await talentLayerService.services(6) + + expect(tx) + .to.emit(talentLayerService, 'ServiceCreated') + .withArgs(6, aliceTlId, alicePlatformId, cid, ethers.constants.AddressZero, referralAmount) + + expect(service6Data.status).to.be.equal(ServiceStatus.Opened) + expect(service6Data.ownerId).to.be.equal(aliceTlId) + expect(service6Data.rateToken).to.be.equal(ethers.constants.AddressZero) + expect(service6Data.dataUri).to.be.equal(cid) + expect(service6Data.platformId).to.be.equal(1) + expect(service6Data.referralAmount).to.be.equal(referralAmount) + }) + + it('Alice the buyer can create a service using ERC20 Token with referral', async function () { + const platform = await talentLayerPlatformID.getPlatform(alicePlatformId) + const alicePlatformServicePostingFee = platform.servicePostingFee + + const signature = await getSignatureForService(platformOneOwner, aliceTlId, 6, cid) + const tx = await talentLayerService + .connect(alice) + .createService(aliceTlId, alicePlatformId, cid, signature, token.address, referralAmount, { + value: alicePlatformServicePostingFee, + }) + const service7Data = await talentLayerService.services(7) + + expect(tx) + .to.emit(talentLayerService, 'ServiceCreated') + .withArgs(7, aliceTlId, alicePlatformId, cid, token.address, referralAmount) + + expect(service7Data.status).to.be.equal(ServiceStatus.Opened) + expect(service7Data.ownerId).to.be.equal(aliceTlId) + expect(service7Data.rateToken).to.be.equal(token.address) + expect(service7Data.dataUri).to.be.equal(cid) + expect(service7Data.platformId).to.be.equal(1) + expect(service7Data.referralAmount).to.be.equal(referralAmount) }) it("Alice can't create a new open service with wrong TalentLayer Platform ID", async function () { const signature = await getSignatureForService(platformOneOwner, aliceTlId, 5, cid) await expect( - talentLayerService.connect(alice).createService(aliceTlId, 5, 'wrongTlPid', signature), + talentLayerService + .connect(alice) + .createService(aliceTlId, 5, 'wrongTlPid', signature, token.address, 0), ).to.be.revertedWith('Invalid platform ID') }) it('Alice can update her service data', async function () { - await talentLayerService.connect(alice).updateServiceData(aliceTlId, 1, cid2) + await talentLayerService.connect(alice).updateService(aliceTlId, 1, referralAmount, cid2) const serviceData = await talentLayerService.services(1) expect(serviceData.dataUri).to.be.equal(cid2) + + const newReferralAmount = 40000 + const tx = await talentLayerService + .connect(alice) + .updateService(aliceTlId, 6, newReferralAmount, cid2) + const service6Data = await talentLayerService.services(6) + + expect(tx) + .to.emit(talentLayerService, 'ServiceUpdated') + .withArgs(6, cid2, newReferralAmount) + expect(service6Data.referralAmount).to.be.equal(newReferralAmount) + }) + + it('Alice cannot update her service with an inferior referral amount', async function () { + await expect( + talentLayerService.connect(alice).updateService(aliceTlId, 1, referralAmount - 1, cid2), + ).to.be.revertedWith("Can't reduce referral amount") }) it('Alice can cancel her own service', async function () { @@ -867,12 +975,12 @@ describe('TalentLayer protocol global testing', function () { .createProposal( aliceTlId, 1, - rateToken, 15, alicePlatformId, cid, proposalExpirationDate, signature, + 0, { value: alicePlatformProposalPostingFee, }, @@ -888,16 +996,7 @@ describe('TalentLayer protocol global testing', function () { await expect( talentLayerService .connect(bob) - .createProposal( - bobTlId, - 5, - rateToken, - 15, - bobPlatformId, - cid, - proposalExpirationDate, - signature, - ), + .createProposal(bobTlId, 5, 15, bobPlatformId, cid, proposalExpirationDate, signature, 0), ).to.be.revertedWith('Service not opened') }) @@ -910,14 +1009,13 @@ describe('TalentLayer protocol global testing', function () { it('Bob cannot update his proposal since it does not exist yet', async function () { const tx = talentLayerService .connect(bob) - .updateProposal(bobTlId, 1, rateToken, 18, cid, proposalExpirationDate) + .updateProposal(bobTlId, 1, 18, cid, proposalExpirationDate, aliceTlId) await expect(tx).to.be.revertedWith('Not the owner') }) - it('Bob can t create a proposal with an amount under the transcation limit amount ', async function () { + it('Bob can t create a proposal with an amount under the transaction limit amount ', async function () { // Proposal on the Open service n 1 - const rateToken = '0xC01FcDfDE3B2ABA1eab76731493C617FfAED2F10' const platform = await talentLayerPlatformID.getPlatform(alicePlatformId) const alicePlatformProposalPostingFee = platform.servicePostingFee @@ -928,13 +1026,13 @@ describe('TalentLayer protocol global testing', function () { .connect(bob) .createProposal( bobTlId, - 1, - rateToken, - 9, + 3, + 6, alicePlatformId, cid, proposalExpirationDate, signature, + 0, { value: alicePlatformProposalPostingFee, }, @@ -953,12 +1051,12 @@ describe('TalentLayer protocol global testing', function () { .createProposal( bobTlId, 1, - rateToken, 15, alicePlatformId, cid2, proposalExpirationDate, signature, + 0, { value: alicePlatformProposalPostingFee.sub(1), }, @@ -979,17 +1077,17 @@ describe('TalentLayer protocol global testing', function () { // Bob creates a proposal on Platform 1 const signature = await getSignatureForProposal(platformOneOwner, bobTlId, 1, cid2) - await talentLayerService + const tx = await talentLayerService .connect(bob) .createProposal( bobTlId, 1, - rateToken, 15, alicePlatformId, cid2, proposalExpirationDate, signature, + 0, { value: alicePlatformProposalPostingFee, }, @@ -1003,12 +1101,157 @@ describe('TalentLayer protocol global testing', function () { expect(serviceData.ownerId).to.be.equal(aliceTlId) // Proposal data check after the proposal - - expect(proposalDataAfter.rateToken).to.be.equal(rateToken) + expect(proposalDataAfter.rateToken).to.be.equal(serviceData.rateToken) expect(proposalDataAfter.rateAmount.toString()).to.be.equal('15') expect(proposalDataAfter.dataUri).to.be.equal(cid2) + expect(proposalDataAfter.platformId).to.be.equal(alicePlatformId) + expect(proposalDataAfter.expirationDate).to.be.equal(proposalExpirationDate) expect(proposalDataAfter.ownerId).to.be.equal(bobTlId) expect(proposalDataAfter.status.toString()).to.be.equal('0') + expect(proposalDataAfter.referrerId).to.be.equal(0) + expect(tx) + .to.emit(talentLayerService, 'ProposalCreated') + .withArgs(1, bobTlId, cid2, 'Pending', 15, alicePlatformId, proposalExpirationDate, 0) + }) + + it("Carol can't create a proposal with a non existing referrer id", async function () { + // Proposal on the Open service n 1 + const platform = await talentLayerPlatformID.getPlatform(alicePlatformId) + const alicePlatformProposalPostingFee = platform.servicePostingFee + + const signature = await getSignatureForProposal(platformOneOwner, carolTlId, 4, cid2) + const tx = talentLayerService + .connect(carol) + .createProposal( + carolTlId, + 1, + 15, + alicePlatformId, + cid2, + proposalExpirationDate, + signature, + 99, + { + value: alicePlatformProposalPostingFee, + }, + ) + await expect(tx).to.be.revertedWith('not valid') + }) + + it('Carol can create a proposal with a referrer for a service using ETH', async function () { + // Proposal on the Open service n 6 + const platform = await talentLayerPlatformID.getPlatform(alicePlatformId) + const alicePlatformProposalPostingFee = platform.servicePostingFee + + // Proposal data check before the proposal + const proposalDataBefore = await talentLayerService.getProposal(6, carolTlId) + expect(proposalDataBefore.ownerId.toString()).to.be.equal('0') + + // Carol creates a proposal on Platform 1 + const signature = await getSignatureForProposal(platformOneOwner, carolTlId, 6, cid2) + const tx = await talentLayerService + .connect(carol) + .createProposal( + carolTlId, + 6, + 2000000, + alicePlatformId, + cid2, + proposalExpirationDate, + signature, + bobTlId, + { + value: alicePlatformProposalPostingFee, + }, + ) + + const serviceData = await talentLayerService.services(6) + const proposalDataAfter = await talentLayerService.getProposal(6, carolTlId) + + // Service data check + expect(serviceData.status).to.be.equal(ServiceStatus.Opened) + expect(serviceData.ownerId).to.be.equal(aliceTlId) + + // Proposal data check after the proposal + // @dev: token field not used any more + expect(proposalDataAfter.rateToken).to.be.equal(serviceData.rateToken) + expect(proposalDataAfter.rateAmount.toString()).to.be.equal('2000000') + expect(proposalDataAfter.dataUri).to.be.equal(cid2) + expect(proposalDataAfter.platformId).to.be.equal(alicePlatformId) + expect(proposalDataAfter.expirationDate).to.be.equal(proposalExpirationDate) + expect(proposalDataAfter.ownerId).to.be.equal(carolTlId) + expect(proposalDataAfter.status.toString()).to.be.equal('0') + expect(proposalDataAfter.referrerId.toString()).to.be.equal(bobTlId.toString()) + expect(tx) + .to.emit(talentLayerService, 'ProposalCreated') + .withArgs( + 6, + carolTlId, + cid2, + 'Pending', + 2000000, + alicePlatformId, + proposalExpirationDate, + bob, + ) + }) + + it('Carol can create a proposal with a referrer for a service using ERC20 token', async function () { + // Proposal on the Open service n 7 + const platform = await talentLayerPlatformID.getPlatform(alicePlatformId) + const alicePlatformProposalPostingFee = platform.servicePostingFee + + // Proposal data check before the proposal + const proposalDataBefore = await talentLayerService.getProposal(4, carolTlId) + expect(proposalDataBefore.ownerId.toString()).to.be.equal('0') + + // Carol creates a proposal on Platform 1 + const signature = await getSignatureForProposal(platformOneOwner, carolTlId, 7, cid2) + const tx = await talentLayerService + .connect(carol) + .createProposal( + carolTlId, + 7, + 2000000, + alicePlatformId, + cid2, + proposalExpirationDate, + signature, + bobTlId, + { + value: alicePlatformProposalPostingFee, + }, + ) + + const serviceData = await talentLayerService.services(7) + const proposalDataAfter = await talentLayerService.getProposal(7, carolTlId) + + // Service data check + expect(serviceData.status).to.be.equal(ServiceStatus.Opened) + expect(serviceData.ownerId).to.be.equal(aliceTlId) + + // Proposal data check after the proposal + // @dev: token field not used any more + expect(proposalDataAfter.rateToken).to.be.equal(serviceData.rateToken) + expect(proposalDataAfter.rateAmount.toString()).to.be.equal('2000000') + expect(proposalDataAfter.dataUri).to.be.equal(cid2) + expect(proposalDataAfter.platformId).to.be.equal(alicePlatformId) + expect(proposalDataAfter.expirationDate).to.be.equal(proposalExpirationDate) + expect(proposalDataAfter.ownerId).to.be.equal(carolTlId) + expect(proposalDataAfter.status.toString()).to.be.equal('0') + expect(proposalDataAfter.referrerId.toString()).to.be.equal(bobTlId.toString()) + expect(tx) + .to.emit(talentLayerService, 'ProposalCreated') + .withArgs( + 7, + carolTlId, + cid2, + 'Pending', + 2000000, + alicePlatformId, + proposalExpirationDate, + bobTlId, + ) }) it("Bob can't create another proposal for the same service", async function () { @@ -1022,12 +1265,12 @@ describe('TalentLayer protocol global testing', function () { .createProposal( bobTlId, 1, - rateToken, 15, alicePlatformId, cid2, proposalExpirationDate, signature, + 0, { value: alicePlatformProposalPostingFee, }, @@ -1046,22 +1289,12 @@ describe('TalentLayer protocol global testing', function () { const signature = await getSignatureForProposal(platformOneOwner, eveTid.toNumber(), 1, cid2) await talentLayerService .connect(eve) - .createProposal( - eveTid, - 1, - rateToken, - 15, - alicePlatformId, - cid2, - expiredProposalDate, - signature, - { - value: alicePlatformProposalPostingFee, - }, - ) + .createProposal(eveTid, 1, 15, alicePlatformId, cid2, expiredProposalDate, signature, 0, { + value: alicePlatformProposalPostingFee, + }) }) - it('Alice should note be able to validate the Eve proposal after the expiration date', async function () { + it('Alice should not be able to validate the Eve proposal after the expiration date', async function () { await expect( talentLayerEscrow.connect(alice).createTransaction(1, 4, metaEvidenceCid, cid2), ).to.be.revertedWith('Proposal expired') @@ -1078,12 +1311,12 @@ describe('TalentLayer protocol global testing', function () { .createProposal( carolTlId, 1, - rateToken, 16, bobPlatformId, cid2, proposalExpirationDate, signature, + 0, { value: bobPlatformProposalPostingFee, }, @@ -1094,44 +1327,30 @@ describe('TalentLayer protocol global testing', function () { await talentLayerService.getProposal(1, carolTid) }) - it('Should revert if Carol tries to create a proposal with a non-whitelisted payment token', async function () { - const platform = await talentLayerPlatformID.getPlatform(alicePlatformId) - const alicePlatformProposalPostingFee = platform.proposalPostingFee - - const signature = await getSignatureForProposal(platformOneOwner, carolTlId, 1, cid2) - await expect( - talentLayerService - .connect(carol) - .createProposal( - carolTlId, - 1, - nonListedRateToken, - 16, - alicePlatformId, - cid2, - proposalExpirationDate, - signature, - { value: alicePlatformProposalPostingFee }, - ), - ).to.be.revertedWith('Token not allowed') - }) - it('Bob can update his first proposal ', async function () { const proposalDataBefore = await talentLayerService.getProposal(1, bobTlId) expect(proposalDataBefore.rateAmount.toString()).to.be.equal('15') - await talentLayerService + const serviceData = await talentLayerService.services(1) + + const tx = await talentLayerService .connect(bob) - .updateProposal(bobTlId, 1, rateToken, 18, cid, proposalExpirationDate) + .updateProposal(bobTlId, 1, 18, cid, proposalExpirationDate, aliceTlId) const proposalDataAfter = await talentLayerService.getProposal(1, bobTlId) + expect(tx) + .to.emit(talentLayerService, 'ProposalUpdated') + .withArgs(1, bobTlId, cid, 18, proposalExpirationDate) expect(proposalDataAfter.rateAmount.toString()).to.be.equal('18') + expect(proposalDataAfter.expirationDate).to.be.equal(proposalExpirationDate) + expect(proposalDataAfter.rateToken.toString()).to.be.equal(serviceData.rateToken) expect(proposalDataAfter.dataUri).to.be.equal(cid) + expect(proposalDataAfter.referrerId).to.be.equal(aliceTlId) }) it("Bob can't update his proposal with an amount which is too low", async function () { const minTransactionAmount = await ( - await talentLayerService.allowedTokenList(rateToken) + await talentLayerService.allowedTokenList(token.address) ).minimumTransactionAmount const tx = talentLayerService @@ -1139,21 +1358,25 @@ describe('TalentLayer protocol global testing', function () { .updateProposal( bobTlId, 1, - rateToken, minTransactionAmount.sub(1), cid, proposalExpirationDate, + aliceTlId, ) await expect(tx).to.be.revertedWith('Amount too low') }) - it('Should revert if Bob updates his proposal with a non-whitelisted payment token ', async function () { - await expect( - talentLayerService - .connect(bob) - .updateProposal(bobTlId, 1, nonListedRateToken, 2, cid2, proposalExpirationDate), - ).to.be.revertedWith('Token not allowed') + it("Bob can't update his proposal with a non-existing referrer id", async function () { + const minTransactionAmount = await ( + await talentLayerService.allowedTokenList(token.address) + ).minimumTransactionAmount + + const tx = talentLayerService + .connect(bob) + .updateProposal(bobTlId, 1, minTransactionAmount, cid, proposalExpirationDate, 99) + + await expect(tx).to.be.revertedWith('not valid') }) }) @@ -1189,12 +1412,12 @@ describe('TalentLayer protocol global testing', function () { .createProposal( bobTlId, serviceId, - token.address, amountBob, bobPlatformId, cid, proposalExpirationDate, signature, + 0, { value: bobPlatformProposalPostingFee }, ) }) @@ -1211,12 +1434,12 @@ describe('TalentLayer protocol global testing', function () { .createProposal( carolTlId, serviceId, - token.address, amountCarol, bobPlatformId, cid, proposalExpirationDate, signature, + 0, { value: bobPlatformProposalPostingFee }, ) }) @@ -1229,12 +1452,12 @@ describe('TalentLayer protocol global testing', function () { .createProposal( carolTlId, 99, - ethers.constants.AddressZero, 1000, bobPlatformId, cid, proposalExpirationDate, signature2, + 0, ) await expect(tx).to.revertedWith('Service not exist') }) @@ -1280,7 +1503,7 @@ describe('TalentLayer protocol global testing', function () { amountBob + (amountBob * (protocolEscrowFeeRate + originValidatedProposalFeeRate + originServiceFeeRate)) / - 10000 + FEE_DIVIDER await token.connect(alice).approve(talentLayerEscrow.address, totalAmount) @@ -1312,7 +1535,9 @@ describe('TalentLayer protocol global testing', function () { }) it('Alice cannot update her service data after it is confirmed', async function () { - const tx = talentLayerService.connect(alice).updateServiceData(aliceTlId, serviceId, cid2) + const tx = talentLayerService + .connect(alice) + .updateService(aliceTlId, serviceId, referralAmount, cid2) await expect(tx).to.be.revertedWith('status must be opened') }) @@ -1332,12 +1557,12 @@ describe('TalentLayer protocol global testing', function () { .createProposal( carolTlId, serviceId, - rateToken, 15, alicePlatformId, cid2, proposalExpirationDate, signature, + 0, { value: alicePlatformProposalPostingFee, }, @@ -1349,7 +1574,7 @@ describe('TalentLayer protocol global testing', function () { it('Bob cannot update his proposal after the service is confirmed', async function () { const tx = talentLayerService .connect(bob) - .updateProposal(bobTlId, serviceId, rateToken, 18, cid, proposalExpirationDate) + .updateProposal(bobTlId, serviceId, 18, cid, proposalExpirationDate, aliceTlId) await expect(tx).to.be.revertedWith('Service not opened') }) @@ -1386,6 +1611,184 @@ describe('TalentLayer protocol global testing', function () { await expect(tx).to.be.revertedWith('Amount too low') }) + it("Alice can deposit funds for Carol's proposal for service 7 including referral amount, which will emit an event.", async function () { + const service = await talentLayerService.services(7) + const proposal = await talentLayerService.proposals(7, carolTlId) + const alicePlatformData = await talentLayerPlatformID.platforms(alicePlatformId) + // const bobPlatformData = await talentLayerPlatformID.platforms(bobPlatformId) + const protocolEscrowFeeRate = await talentLayerEscrow.protocolEscrowFeeRate() + const originServiceFeeRate = alicePlatformData.originServiceFeeRate + const originValidatedProposalFeeRate = alicePlatformData.originValidatedProposalFeeRate + const referralAmount = service.referralAmount + const totalAmountWithoutFees = proposal.rateAmount.add(referralAmount) + + const totalAmountWithReferral = totalAmountWithoutFees.add( + totalAmountWithoutFees + .mul(protocolEscrowFeeRate) + .add(totalAmountWithoutFees.mul(originServiceFeeRate)) + .add(totalAmountWithoutFees.mul(originValidatedProposalFeeRate)) + .div(FEE_DIVIDER), + ) + + await token.connect(alice).approve(talentLayerEscrow.address, totalAmountWithReferral) + + // Success if the value sent is the total amount - transactionId: 2 + const tx = await talentLayerEscrow + .connect(alice) + .createTransaction(7, carolTlId, metaEvidenceCid, proposal.dataUri) + await expect(tx).to.changeTokenBalances( + token, + [talentLayerEscrow.address, alice, bob], + [totalAmountWithReferral, -totalAmountWithReferral, 0], + ) + + await expect(tx) + .to.emit(talentLayerEscrow, 'TransactionCreated') + .to.emit(talentLayerEscrow, 'MetaEvidence') + }) + + it('Alice can release 50% of the escrow to Carol for service 7 which includes a referral amount, and fees are correctly split.', async function () { + const transactionDetails = await talentLayerEscrow.connect(alice).getTransactionDetails(2) + const protocolEscrowFeeRate = transactionDetails.protocolEscrowFeeRate + const originServiceFeeRate = transactionDetails.originServiceFeeRate + const originValidatedProposalFeeRate = transactionDetails.originValidatedProposalFeeRate + const releaseAmount = transactionDetails.amount + .add(transactionDetails.releasedAmount) + .mul(BigNumber.from(1)) + .div(BigNumber.from(2)) + const releasedReferrerAmount = releaseAmount + .mul(transactionDetails.referralAmount) + .div(transactionDetails.amount) + + const tx = await talentLayerEscrow.connect(alice).release(aliceTlId, 2, releaseAmount) + // Bob gets half of the referral amount + await expect(tx).to.changeTokenBalances( + token, + [talentLayerEscrow.address, alice, carol], + [-releaseAmount, 0, releaseAmount], + ) + + const alicePlatformBalance = await talentLayerEscrow + .connect(alice) + .getClaimableFeeBalance(token.address) + const deployerBalance = await talentLayerEscrow + .connect(deployer) + .getClaimableFeeBalance(token.address) + const referrerBalance = await talentLayerEscrow + .connect(bob) + .getClaimableReferralBalance(token.address) + + // Alice gets half of both the originServiceFeeRate and the originValidatedProposalFeeRate + expect(alicePlatformBalance).to.be.equal( + releaseAmount.mul(originServiceFeeRate + originValidatedProposalFeeRate).div(FEE_DIVIDER), + ) + expect(deployerBalance).to.be.equal( + releaseAmount.mul(protocolEscrowFeeRate).div(FEE_DIVIDER), + ) + expect(referrerBalance).to.be.equal(releasedReferrerAmount) + const aliceClaimTx = await talentLayerEscrow + .connect(alice) + .claim(alicePlatformId, token.address) + const deployerClaimTx = await talentLayerEscrow.connect(deployer).claim(0, token.address) + const bobReferrerClaimTx = await talentLayerEscrow + .connect(bob) + .claimReferralBalance(bobTlId, token.address) + + expect(deployerClaimTx).to.changeTokenBalances(token, [alice], [alicePlatformBalance]) + expect(aliceClaimTx).to.changeTokenBalances(token, [deployer], [deployerBalance]) + expect(bobReferrerClaimTx).to.changeTokenBalances(token, [bob], [referrerBalance]) + }) + + it('Alice can reimburse 40% of the escrow to Carol for service 7 which includes a referral amount.', async function () { + const transactionDetails = await talentLayerEscrow.connect(alice).getTransactionDetails(2) + const protocolEscrowFeeRate = transactionDetails.protocolEscrowFeeRate + const originServiceFeeRate = transactionDetails.originServiceFeeRate + const originValidatedProposalFeeRate = transactionDetails.originValidatedProposalFeeRate + const reimburseAmount = transactionDetails.totalAmount + .mul(BigNumber.from(4)) + .div(BigNumber.from(10)) + + const reimburseReferrerAmount = reimburseAmount + .mul(transactionDetails.referralAmount) + .div(transactionDetails.totalAmount) + + const reimburseAmountWithReferrer = reimburseAmount.add(reimburseReferrerAmount) + const reimburseFeesAMount = reimburseAmountWithReferrer + .mul(protocolEscrowFeeRate) + .add(reimburseAmountWithReferrer.mul(originServiceFeeRate)) + .add(reimburseAmountWithReferrer.mul(originValidatedProposalFeeRate)) + .div(FEE_DIVIDER) + + const totalReimburseAmount = reimburseAmount + .add(reimburseReferrerAmount) + .add(reimburseFeesAMount) + + const tx = await talentLayerEscrow.connect(carol).reimburse(carolTlId, 2, reimburseAmount) + // Alice gets back: 40% of all 3 fees + 40% of referralAmount + await expect(tx).to.changeTokenBalances( + token, + [talentLayerEscrow.address, alice], + [-totalReimburseAmount, totalReimburseAmount], + ) + }) + + it('Alice can release the remaining 10% of the escrow (inferior to FEE_DIVIDER value) to Carol, and fees are correctly split, including referral amount.', async function () { + const transactionDetails = await talentLayerEscrow.connect(alice).getTransactionDetails(2) + const protocolEscrowFeeRate = transactionDetails.protocolEscrowFeeRate + const originServiceFeeRate = transactionDetails.originServiceFeeRate + const originValidatedProposalFeeRate = transactionDetails.originValidatedProposalFeeRate + const releaseAmount = transactionDetails.amount + const releasedReferrerAmount = releaseAmount + .mul(transactionDetails.referralAmount) + .div(transactionDetails.totalAmount) + + const tx = await talentLayerEscrow.connect(alice).release(aliceTlId, 2, releaseAmount) + // Bob gets 10% of the referral amount + await expect(tx).to.changeTokenBalances( + token, + [talentLayerEscrow.address, alice, carol], + [-releaseAmount, 0, releaseAmount], + ) + + const alicePlatformBalance = await talentLayerEscrow + .connect(alice) + .getClaimableFeeBalance(token.address) + const deployerBalance = await talentLayerEscrow + .connect(deployer) + .getClaimableFeeBalance(token.address) + const referrerBalance = await talentLayerEscrow + .connect(bob) + .getClaimableReferralBalance(token.address) + + // Alice gets 10% of both the originServiceFeeRate and the originValidatedProposalFeeRate + expect(alicePlatformBalance).to.be.equal( + releaseAmount.mul(originServiceFeeRate + originValidatedProposalFeeRate).div(FEE_DIVIDER), + ) + expect(deployerBalance.toString()).to.be.equal( + releaseAmount.mul(protocolEscrowFeeRate).div(FEE_DIVIDER), + ) + expect(referrerBalance.toString()).to.be.equal(releasedReferrerAmount) + const deployerClaimTx = await talentLayerEscrow + .connect(alice) + .claim(alicePlatformId, token.address) + const aliceClaimTx = await talentLayerEscrow.connect(deployer).claim(0, token.address) + const bobReferrerClaimTx = await talentLayerEscrow + .connect(bob) + .claimReferralBalance(bobTlId, token.address) + + expect(deployerClaimTx).to.changeTokenBalances(token, [alice], [alicePlatformBalance]) + expect(aliceClaimTx).to.changeTokenBalances(token, [deployer], [deployerBalance]) + expect(bobReferrerClaimTx).to.changeTokenBalances(token, [bob], [referrerBalance]) + + // Service status is finished + const serviceData = await talentLayerService.services(transactionDetails.serviceId) + const transactionDetailsAfter = await talentLayerEscrow + .connect(alice) + .getTransactionDetails(2) + expect(serviceData.status).to.equal(2) + expect(transactionDetailsAfter.amount).to.equal(0) + }) + it('Alice can release half of the escrow to bob, and fees are correctly split.', async function () { const transactionDetailsBefore = await talentLayerEscrow .connect(alice) @@ -1435,14 +1838,14 @@ describe('TalentLayer protocol global testing', function () { .getClaimableFeeBalance(token.address) // Alice gets originServiceFeeRate as the service was created on her platform expect(alicePlatformBalance.toString()).to.be.equal( - ((releasedAmount * originServiceFeeRate) / 10000).toString(), + ((releasedAmount * originServiceFeeRate) / FEE_DIVIDER).toString(), ) // Bob gets originProposalValidatedFeeRate as the proposal was validated on his platform expect(bobPlatformBalance.toString()).to.be.equal( - ((releasedAmount * originValidatedProposalFeeRate) / 10000).toString(), + ((releasedAmount * originValidatedProposalFeeRate) / FEE_DIVIDER).toString(), ) expect(deployerBalance.toString()).to.be.equal( - ((releasedAmount * protocolEscrowFeeRate) / 10000).toString(), + ((releasedAmount * protocolEscrowFeeRate) / FEE_DIVIDER).toString(), ) }) @@ -1488,14 +1891,14 @@ describe('TalentLayer protocol global testing', function () { .getClaimableFeeBalance(token.address) // Alice gets originServiceFeeRate as the service was created on her platform expect(alicePlatformBalance.toString()).to.be.equal( - ((((3 * amountBob) / 4) * originServiceFeeRate) / 10000).toString(), + ((((3 * amountBob) / 4) * originServiceFeeRate) / FEE_DIVIDER).toString(), ) // Bob gets originProposalValidatedFeeRate as the proposal was validated on his platform expect(bobPlatformBalance.toString()).to.be.equal( - ((((3 * amountBob) / 4) * originValidatedProposalFeeRate) / 10000).toString(), + ((((3 * amountBob) / 4) * originValidatedProposalFeeRate) / FEE_DIVIDER).toString(), ) expect(deployerBalance.toString()).to.be.equal( - ((((3 * amountBob) / 4) * protocolEscrowFeeRate) / 10000).toString(), + ((((3 * amountBob) / 4) * protocolEscrowFeeRate) / FEE_DIVIDER).toString(), ) }) @@ -1505,11 +1908,13 @@ describe('TalentLayer protocol global testing', function () { const alicePlatformProposalPostingFee = platform.proposalPostingFee // Create the service - const serviceId = 6 + const serviceId = 8 const signature = await getSignatureForService(platformOneOwner, aliceTlId, 5, cid) - await talentLayerService.connect(alice).createService(aliceTlId, 1, cid, signature, { - value: alicePlatformServicePostingFee, - }) + await talentLayerService + .connect(alice) + .createService(aliceTlId, 1, cid, signature, token.address, 0, { + value: alicePlatformServicePostingFee, + }) await talentLayerService.services(serviceId) // Create the proposal @@ -1520,12 +1925,12 @@ describe('TalentLayer protocol global testing', function () { .createProposal( bobTlId, serviceId, - rateToken, 15, alicePlatformId, cid, proposalExpirationDate, signature2, + 0, { value: alicePlatformProposalPostingFee, }, @@ -1543,7 +1948,7 @@ describe('TalentLayer protocol global testing', function () { amountBob + (amountBob * (protocolEscrowFeeRate + originServiceFeeRate + originValidatedProposalFeeRate)) / - 10000 + FEE_DIVIDER await token.connect(alice).approve(talentLayerEscrow.address, totalAmount) @@ -1652,7 +2057,7 @@ describe('TalentLayer protocol global testing', function () { const amountBob = 1000000 const amountCarol = 200 const serviceId = 3 - const transactionId = 2 + const transactionId = 3 let proposalIdBob = 0 //Will be set later let proposalIdCarol = 0 //Will be set later let totalAmount = 0 //Will be set later @@ -1675,7 +2080,7 @@ describe('TalentLayer protocol global testing', function () { amountBob + (amountBob * (protocolEscrowFeeRate + originValidatedProposalFeeRate + originServiceFeeRate)) / - 10000 + FEE_DIVIDER // we need to retreive the Bob proposal dataUri const proposal = await talentLayerService.proposals(serviceId, bobTlId) @@ -1700,12 +2105,12 @@ describe('TalentLayer protocol global testing', function () { .createProposal( bobTlId, serviceId, - ethAddress, amountBob, bobPlatformId, cid, proposalExpirationDate, signature, + 0, { value: bobPlatformProposalPostingFee }, ) }) @@ -1721,21 +2126,21 @@ describe('TalentLayer protocol global testing', function () { .createProposal( carolTlId, serviceId, - ethAddress, amountCarol, bobPlatformId, cid, proposalExpirationDate, signature, + 0, { value: bobPlatformProposalPostingFee }, ) }) - // bob will try to frunt run the proposal by changing the proposal dataUri + // bob will try to front run the proposal by changing the proposal dataUri it('Bob will try to front run the proposal validation by changing the proposal dataUri.', async function () { await talentLayerService .connect(bob) - .updateProposal(bobTlId, serviceId, ethAddress, amountBob, cid2, proposalExpirationDate) + .updateProposal(bobTlId, serviceId, amountBob, cid2, proposalExpirationDate, aliceTlId) await expect( talentLayerEscrow @@ -1794,6 +2199,187 @@ describe('TalentLayer protocol global testing', function () { ).to.be.reverted }) + it("Alice can deposit funds for Carol's proposal for service 6 including referral amount, which will emit an event.", async function () { + const service = await talentLayerService.services(6) + const proposal = await talentLayerService.proposals(6, carolTlId) + const alicePlatformData = await talentLayerPlatformID.platforms(alicePlatformId) + // const bobPlatformData = await talentLayerPlatformID.platforms(bobPlatformId) + const protocolEscrowFeeRate = await talentLayerEscrow.protocolEscrowFeeRate() + const originServiceFeeRate = alicePlatformData.originServiceFeeRate + const originValidatedProposalFeeRate = alicePlatformData.originValidatedProposalFeeRate + const referralAmount = service.referralAmount + const totalAmountWithoutFees = proposal.rateAmount.add(referralAmount) + + const totalAmountWithReferral = totalAmountWithoutFees.add( + totalAmountWithoutFees + .mul(protocolEscrowFeeRate) + .add(totalAmountWithoutFees.mul(originServiceFeeRate)) + .add(totalAmountWithoutFees.mul(originValidatedProposalFeeRate)) + .div(FEE_DIVIDER), + ) + + // Fails if value sent is not the total amount + const tx = talentLayerEscrow + .connect(alice) + .createTransaction(6, carolTlId, metaEvidenceCid, proposal.dataUri, { + value: totalAmountWithReferral.sub(BigNumber.from(1)), + }) + await expect(tx).to.be.revertedWith('Non-matching funds') + + // Success if the value sent is the total amount - transactionId: 4 + const tx2 = await talentLayerEscrow + .connect(alice) + .createTransaction(6, carolTlId, metaEvidenceCid, proposal.dataUri, { + value: totalAmountWithReferral, + }) + await expect(tx2).to.changeEtherBalances( + [talentLayerEscrow.address, alice, bob], + [totalAmountWithReferral, -totalAmountWithReferral, 0], + ) + + await expect(tx2) + .to.emit(talentLayerEscrow, 'TransactionCreated') + .to.emit(talentLayerEscrow, 'MetaEvidence') + }) + + it('Alice can release 50% of the escrow to Carol for service 6 which includes a referral amount, and fees are correctly split.', async function () { + const transactionDetails = await talentLayerEscrow.connect(alice).getTransactionDetails(4) + const protocolEscrowFeeRate = transactionDetails.protocolEscrowFeeRate + const originServiceFeeRate = transactionDetails.originServiceFeeRate + const originValidatedProposalFeeRate = transactionDetails.originValidatedProposalFeeRate + const releaseAmount = transactionDetails.amount + .add(transactionDetails.releasedAmount) + .mul(BigNumber.from(1)) + .div(BigNumber.from(2)) + const releasedReferrerAmount = releaseAmount + .mul(transactionDetails.referralAmount) + .div(transactionDetails.amount) + + const tx = await talentLayerEscrow.connect(alice).release(aliceTlId, 4, releaseAmount) + // Bob gets half of the referral amount + await expect(tx).to.changeEtherBalances( + [talentLayerEscrow.address, alice, carol], + [-releaseAmount, 0, releaseAmount], + ) + + const alicePlatformBalance = await talentLayerEscrow + .connect(alice) + .getClaimableFeeBalance(ethAddress) + const deployerBalance = await talentLayerEscrow + .connect(deployer) + .getClaimableFeeBalance(ethAddress) + const referrerBalance = await talentLayerEscrow + .connect(bob) + .getClaimableReferralBalance(ethAddress) + // Alice gets half of both the originServiceFeeRate and the originValidatedProposalFeeRate + expect(alicePlatformBalance).to.be.equal( + releaseAmount.mul(originServiceFeeRate + originValidatedProposalFeeRate).div(FEE_DIVIDER), + ) + expect(deployerBalance.toString()).to.be.equal( + releaseAmount.mul(protocolEscrowFeeRate).div(FEE_DIVIDER), + ) + expect(referrerBalance).to.be.equal(releasedReferrerAmount) + const aliceClaimTx = await talentLayerEscrow + .connect(alice) + .claim(alicePlatformId, ethAddress) + const deployerClaimTx = await talentLayerEscrow.connect(deployer).claim(0, ethAddress) + const bobReferrerClaimTx = await talentLayerEscrow + .connect(bob) + .claimReferralBalance(bobTlId, ethAddress) + + expect(deployerClaimTx).to.changeTokenBalances(token, [alice], [alicePlatformBalance]) + expect(aliceClaimTx).to.changeTokenBalances(token, [deployer], [deployerBalance]) + expect(bobReferrerClaimTx).to.changeTokenBalances(token, [bob], [referrerBalance]) + }) + + it('Alice can reimburse 40% of the escrow to Carol for service 6 which includes a referral amount.', async function () { + const transactionDetails = await talentLayerEscrow.connect(alice).getTransactionDetails(4) + const protocolEscrowFeeRate = transactionDetails.protocolEscrowFeeRate + const originServiceFeeRate = transactionDetails.originServiceFeeRate + const originValidatedProposalFeeRate = transactionDetails.originValidatedProposalFeeRate + const reimburseAmount = transactionDetails.totalAmount + .mul(BigNumber.from(4)) + .div(BigNumber.from(10)) + + const reimburseReferrerAmount = reimburseAmount + .mul(transactionDetails.referralAmount) + .div(transactionDetails.totalAmount) + + const reimburseAmountWithReferrer = reimburseAmount.add(reimburseReferrerAmount) + const reimburseFeesAMount = reimburseAmountWithReferrer + .mul(protocolEscrowFeeRate) + .add(reimburseAmountWithReferrer.mul(originServiceFeeRate)) + .add(reimburseAmountWithReferrer.mul(originValidatedProposalFeeRate)) + .div(FEE_DIVIDER) + + const totalReimburseAmount = reimburseAmount + .add(reimburseReferrerAmount) + .add(reimburseFeesAMount) + + const tx = await talentLayerEscrow.connect(carol).reimburse(carolTlId, 4, reimburseAmount) + // Alice gets back: 40% of all 3 fees + 40% of referralAmount + await expect(tx).to.changeEtherBalances( + [talentLayerEscrow.address, alice], + [-totalReimburseAmount, totalReimburseAmount], + ) + }) + + it('Alice can release the remaining 10% of the escrow (inferior to FEE_DIVIDER value) to Carol, and fees are correctly split, including referral amount.', async function () { + const transactionDetails = await talentLayerEscrow.connect(alice).getTransactionDetails(4) + const protocolEscrowFeeRate = transactionDetails.protocolEscrowFeeRate + const originServiceFeeRate = transactionDetails.originServiceFeeRate + const originValidatedProposalFeeRate = transactionDetails.originValidatedProposalFeeRate + const releaseAmount = transactionDetails.amount + const releasedReferrerAmount = releaseAmount + .mul(transactionDetails.referralAmount) + .div(transactionDetails.totalAmount) + + const tx = await talentLayerEscrow.connect(alice).release(aliceTlId, 4, releaseAmount) + // Bob gets 10% of the referral amount + await expect(tx).to.changeEtherBalances( + [talentLayerEscrow.address, alice, carol], + [-releaseAmount, 0, releaseAmount], + ) + + const alicePlatformBalance = await talentLayerEscrow + .connect(alice) + .getClaimableFeeBalance(ethAddress) + const deployerBalance = await talentLayerEscrow + .connect(deployer) + .getClaimableFeeBalance(ethAddress) + const referrerBalance = await talentLayerEscrow + .connect(bob) + .getClaimableReferralBalance(ethAddress) + + // Alice gets 10% of both the originServiceFeeRate and the originValidatedProposalFeeRate + expect(alicePlatformBalance).to.be.equal( + releaseAmount.mul(originServiceFeeRate + originValidatedProposalFeeRate).div(FEE_DIVIDER), + ) + expect(deployerBalance.toString()).to.be.equal( + releaseAmount.mul(protocolEscrowFeeRate).div(FEE_DIVIDER), + ) + expect(referrerBalance.toString()).to.be.equal(releasedReferrerAmount) + const deployerClaimTx = await talentLayerEscrow + .connect(alice) + .claim(alicePlatformId, ethAddress) + const aliceClaimTx = await talentLayerEscrow.connect(deployer).claim(0, ethAddress) + const bobReferrerClaimTx = await talentLayerEscrow + .connect(bob) + .claimReferralBalance(bobTlId, ethAddress) + + expect(deployerClaimTx).to.changeEtherBalances([alice], [alicePlatformBalance]) + expect(aliceClaimTx).to.changeEtherBalances([deployer], [deployerBalance]) + expect(bobReferrerClaimTx).to.changeEtherBalances([bob], [referrerBalance]) + + // Service status is finished + const serviceData = await talentLayerService.services(transactionDetails.serviceId) + const transactionDetailsAfter = await talentLayerEscrow + .connect(alice) + .getTransactionDetails(4) + expect(serviceData.status).to.equal(2) + expect(transactionDetailsAfter.amount).to.equal(0) + }) + it('Carol should not be allowed to release escrow the service.', async function () { await expect( talentLayerEscrow.connect(carol).release(carolTlId, transactionId, 10000), @@ -1828,14 +2414,14 @@ describe('TalentLayer protocol global testing', function () { .getClaimableFeeBalance(ethAddress) // Alice gets the originServiceFeeRate as the service was created on her platform expect(alicePlatformBalance.toString()).to.be.equal( - ((releaseAmount * originServiceFeeRate) / 10000).toString(), + ((releaseAmount * originServiceFeeRate) / FEE_DIVIDER).toString(), ) // Bob gets the originValidatedProposalFeeRate as the proposal was created on his platform expect(bobPlatformBalance.toString()).to.be.equal( - ((releaseAmount * originValidatedProposalFeeRate) / 10000).toString(), + ((releaseAmount * originValidatedProposalFeeRate) / FEE_DIVIDER).toString(), ) expect(deployerBalance.toString()).to.be.equal( - ((releaseAmount * protocolEscrowFeeRate) / 10000).toString(), + ((releaseAmount * protocolEscrowFeeRate) / FEE_DIVIDER).toString(), ) }) @@ -1866,14 +2452,14 @@ describe('TalentLayer protocol global testing', function () { .getClaimableFeeBalance(ethAddress) // Alice gets the originServiceFeeRate as the service was created on her platform expect(alicePlatformBalance.toString()).to.be.equal( - ((((3 * amountBob) / 4) * originServiceFeeRate) / 10000).toString(), + ((((3 * amountBob) / 4) * originServiceFeeRate) / FEE_DIVIDER).toString(), ) // Bob gets the originValidatedProposalFeeRate as the proposal was created on his platform expect(bobPlatformBalance.toString()).to.be.equal( - ((((3 * amountBob) / 4) * originValidatedProposalFeeRate) / 10000).toString(), + ((((3 * amountBob) / 4) * originValidatedProposalFeeRate) / FEE_DIVIDER).toString(), ) expect(deployerBalance.toString()).to.be.equal( - ((((3 * amountBob) / 4) * protocolEscrowFeeRate) / 10000).toString(), + ((((3 * amountBob) / 4) * protocolEscrowFeeRate) / FEE_DIVIDER).toString(), ) }) diff --git a/test/batch/pausableEscrow.ts b/test/batch/pausableEscrow.ts index 70448aef..47269ebc 100644 --- a/test/batch/pausableEscrow.ts +++ b/test/batch/pausableEscrow.ts @@ -40,7 +40,7 @@ async function deployAndSetup(): Promise< , talentLayerService, talentLayerReview, - ] = await deploy(true) + ] = await deploy(false) // Grant Platform Id Mint role to Deployer and Bob const mintRole = await talentLayerPlatformID.MINT_ROLE() @@ -68,7 +68,9 @@ async function deployAndSetup(): Promise< // Alice, the buyer, initiates a new open service const signature = await getSignatureForService(carol, aliceTlId, 0, cid) - await talentLayerService.connect(alice).createService(aliceTlId, carolPlatformId, cid, signature) + await talentLayerService + .connect(alice) + .createService(aliceTlId, carolPlatformId, cid, signature, ethers.constants.AddressZero, 0) // Bob, the seller, creates a proposal for the service const signature2 = await getSignatureForProposal(carol, bobTlId, serviceId, cid) @@ -77,12 +79,12 @@ async function deployAndSetup(): Promise< .createProposal( bobTlId, serviceId, - tokenAddress, transactionAmount, carolPlatformId, cid, proposalExpirationDate, signature2, + 0, ) return [talentLayerEscrow, talentLayerService, talentLayerPlatformID, talentLayerReview] diff --git a/test/batch/platformVerification.ts b/test/batch/platformVerification.ts index 574cf528..4aba075a 100644 --- a/test/batch/platformVerification.ts +++ b/test/batch/platformVerification.ts @@ -119,7 +119,7 @@ describe('Platform verification', function () { const tx = talentLayerService .connect(alice) - .createService(aliceTlId, carolPlatformId, cid, signature) + .createService(aliceTlId, carolPlatformId, cid, signature, ethers.constants.AddressZero, 0) await expect(tx).to.not.reverted }) @@ -129,7 +129,7 @@ describe('Platform verification', function () { const tx = talentLayerService .connect(bob) - .createService(bobTlId, carolPlatformId, cid, signature) + .createService(bobTlId, carolPlatformId, cid, signature, ethers.constants.AddressZero, 0) await expect(tx).to.revertedWith('invalid signature') }) @@ -139,7 +139,7 @@ describe('Platform verification', function () { const tx = talentLayerService .connect(bob) - .createService(bobTlId, carolPlatformId, cid, signature) + .createService(bobTlId, carolPlatformId, cid, signature, ethers.constants.AddressZero, 0) await expect(tx).to.revertedWith('invalid signature') }) @@ -149,7 +149,7 @@ describe('Platform verification', function () { const tx = talentLayerService .connect(bob) - .createService(bobTlId, carolPlatformId, cid, signature) + .createService(bobTlId, carolPlatformId, cid, signature, ethers.constants.AddressZero, 0) await expect(tx).to.revertedWith('invalid signature') }) @@ -159,7 +159,7 @@ describe('Platform verification', function () { const tx = talentLayerService .connect(alice) - .createService(aliceTlId, carolPlatformId, cid, signature) + .createService(aliceTlId, carolPlatformId, cid, signature, ethers.constants.AddressZero, 0) await expect(tx).to.revertedWith('invalid signature') }) @@ -169,7 +169,7 @@ describe('Platform verification', function () { const tx = talentLayerService .connect(alice) - .createService(aliceTlId, carolPlatformId, cid, signature) + .createService(aliceTlId, carolPlatformId, cid, signature, ethers.constants.AddressZero, 0) await expect(tx).to.not.reverted }) @@ -183,12 +183,12 @@ describe('Platform verification', function () { .createProposal( bobTlId, serviceId, - ethers.constants.AddressZero, 1, carolPlatformId, cid, proposalExpirationDate, signature, + 0, ) await expect(tx).to.not.reverted @@ -209,7 +209,7 @@ describe('Platform verification', function () { const tx = await talentLayerService .connect(alice) - .createService(aliceTlId, carolPlatformId, cid, signature) + .createService(aliceTlId, carolPlatformId, cid, signature, ethers.constants.AddressZero, 0) await expect(tx).to.not.reverted }) @@ -227,12 +227,12 @@ describe('Platform verification', function () { .createProposal( bobTlId, serviceId, - ethers.constants.AddressZero, 1, carolPlatformId, cid, proposalExpirationDate, signature, + 0, ) await expect(tx).to.not.reverted diff --git a/test/batch/serviceCompletion.ts b/test/batch/serviceCompletion.ts index 5b8b734e..acfe091a 100644 --- a/test/batch/serviceCompletion.ts +++ b/test/batch/serviceCompletion.ts @@ -41,7 +41,7 @@ async function deployAndSetup(): Promise< , talentLayerService, talentLayerReview, - ] = await deploy(true) + ] = await deploy(false) // Grant Platform Id Mint role to Deployer and Bob const mintRole = await talentLayerPlatformID.MINT_ROLE() @@ -129,7 +129,7 @@ describe('Completion of service', function () { ) await talentLayerService .connect(alice) - .createService(aliceTlId, carolPlatformId, cid, signatureService) + .createService(aliceTlId, carolPlatformId, cid, signatureService, tokenAddress, 0) // Bob, the seller, creates a proposal for the service const signatureProposal = await getSignatureForProposal(carol, bobTlId, serviceId, cid) @@ -138,12 +138,12 @@ describe('Completion of service', function () { .createProposal( bobTlId, serviceId, - tokenAddress, transactionAmount, carolPlatformId, cid, proposalExpirationDate, signatureProposal, + 0, ) // Validate the proposal by locking the funds in the escrow diff --git a/test/batch/transferProfileIds.ts b/test/batch/transferProfileIds.ts index fe6f9288..2986c559 100644 --- a/test/batch/transferProfileIds.ts +++ b/test/batch/transferProfileIds.ts @@ -92,7 +92,9 @@ describe('Transfer of TalentLayer IDs', function () { // Bob creates a new service const signature = await getSignatureForService(eve, bobTlId.toNumber(), 0, cid) - await talentLayerService.connect(bob).createService(bobTlId, evePlatformId, cid, signature) + await talentLayerService + .connect(bob) + .createService(bobTlId, evePlatformId, cid, signature, tokenAddress, 0) expect(await talentLayerID.hasActivity(bobTlId)).to.be.true @@ -124,12 +126,12 @@ describe('Transfer of TalentLayer IDs', function () { .createProposal( carolTlId, serviceId, - tokenAddress, transactionAmount, evePlatformId, cid, proposalExpirationDate, signature, + 0, ) expect(await talentLayerID.hasActivity(carolTlId)).to.be.true diff --git a/test/batch/upgrades/referral.ts b/test/batch/upgrades/referral.ts new file mode 100644 index 00000000..77afd279 --- /dev/null +++ b/test/batch/upgrades/referral.ts @@ -0,0 +1,473 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' +import { + SimpleERC20, + TalentLayerArbitrator, + TalentLayerEscrow, + TalentLayerEscrowV1, + TalentLayerID, + TalentLayerPlatformID, + TalentLayerReview, + TalentLayerService, + TalentLayerServiceV1, +} from '../../../typechain-types' +import { ethers } from 'hardhat' +import { deployForV1, upgradeEscrowV1, upgradeServiceV1 } from '../../utils/deploy' +import { expect } from 'chai' +import { + cid, + metaEvidenceCid, + minTokenWhitelistTransactionAmount, + MintStatus, + proposalExpirationDate, +} from '../../utils/constant' +import { Contract } from 'ethers' + +const aliceTlId = 1 +const bobTlId = 2 +const carolTlId = 3 +const daveTlId = 4 + +const alicePlatformId = 1 + +const rateAmount = 200000 + +const FEE_DIVIDER = 10000 + +// @dev: We are not testing signatures here, so we can use a fake signature +const fakeSignature = + '0xea53f5cd3f7db698f2fdd38909c58fbd41fe35b54d5b0d6acc3b05555bae1f01795b86ea1d65d8c76954fd6cefd5c59c9c57274966a071be1ee3b783a123ff961b' + +const alicePlatformProposalPostingFee = 0 +describe('TalentLayer protocol global testing', function () { + let deployer: SignerWithAddress, + alice: SignerWithAddress, + bob: SignerWithAddress, + carol: SignerWithAddress, + dave: SignerWithAddress, + talentLayerServiceV1: TalentLayerServiceV1, + talentLayerService: TalentLayerService, + talentLayerID: TalentLayerID, + talentLayerPlatformID: TalentLayerPlatformID, + talentLayerReview: TalentLayerReview, + talentLayerEscrowV1: TalentLayerEscrowV1, + talentLayerEscrow: TalentLayerEscrow, + talentLayerArbitrator: TalentLayerArbitrator, + token: SimpleERC20, + platformName: string, + platformId: string + + before(async function () { + // Get the Signers + ;[deployer, alice, bob, carol, dave] = await ethers.getSigners() + ;[ + talentLayerID, + talentLayerPlatformID, + talentLayerEscrowV1, + talentLayerArbitrator, + talentLayerServiceV1, + talentLayerReview, + token, + ] = await deployForV1() + + // Grant Platform Id Mint role to Deployer + const mintRole = await talentLayerPlatformID.MINT_ROLE() + await talentLayerPlatformID.connect(deployer).grantRole(mintRole, deployer.address) + + // we first check the actual minting status (should be ONLY_WHITELIST) + const mintingStatus = await talentLayerPlatformID.connect(deployer).mintStatus() + expect(mintingStatus).to.be.equal(1) + // then we whitelist the deployer and Alice to mint a PlatformId for someone + await talentLayerPlatformID.connect(deployer).whitelistUser(deployer.address) + // we check if the deployer is well whitelisted + const deployerWhitelisted = await talentLayerPlatformID.whitelist(deployer.address) + expect(deployerWhitelisted).to.be.equal(true) + + // Deployer mints Platform Id for Alice + platformName = 'rac00n-corp' + await talentLayerPlatformID.connect(deployer).mintForAddress(platformName, alice.address) + + const allowedTokenList = ['0x0000000000000000000000000000000000000000', token.address] + + // Deployer whitelists a list of authorized tokens + for (const tokenAddress of allowedTokenList) { + await talentLayerServiceV1 + .connect(deployer) + .updateAllowedTokenList(tokenAddress, true, minTokenWhitelistTransactionAmount) + } + + // Disable whitelist for reserved handles + await talentLayerID.connect(deployer).updateMintStatus(MintStatus.PUBLIC) + + // Set service contract address on ID contract + await talentLayerID.connect(deployer).setIsServiceContract(talentLayerServiceV1.address, true) + + // Mint TLIDs for all users + await talentLayerID.connect(deployer).updateMintFee(0) + await talentLayerID.connect(deployer).updateShortHandlesMaxPrice(0) + await talentLayerID.connect(alice).mint('1', 'alice') + await talentLayerID.connect(bob).mint('1', 'bob') + await talentLayerID.connect(carol).mint('1', 'carol') + await talentLayerID.connect(dave).mint('1', 'dave') + + // Transfers SimpleERC20 tokens to users + await token.connect(deployer).transfer(alice.address, 10000000) + await token.connect(deployer).transfer(bob.address, 10000000) + await token.connect(deployer).transfer(dave.address, 10000000) + }) + + describe('Global contract tests', async function () { + it('Alice owns a PlatformId Id minted by the deployer', async function () { + platformId = (await talentLayerPlatformID.ids(alice.address)).toString() + expect(platformId).to.be.equal('1') + }) + it('All users have TLIDs', async function () { + expect(await talentLayerID.ids(alice.address)).to.be.equal(aliceTlId) + expect(await talentLayerID.ids(bob.address)).to.be.equal(bobTlId) + expect(await talentLayerID.ids(carol.address)).to.be.equal(carolTlId) + expect(await talentLayerID.ids(dave.address)).to.be.equal(daveTlId) + }) + }) + + describe('Service & Proposal creation tests - Before Upgrade', async function () { + it('Users can freely create services without providing tokens', async function () { + const platform = await talentLayerPlatformID.getPlatform(alicePlatformId) + const alicePlatformServicePostingFee = platform.servicePostingFee + // @dev: Signature not activated, will use the same signature for all services & proposals + const tx = await talentLayerServiceV1 + .connect(alice) + .createService(aliceTlId, alicePlatformId, cid, fakeSignature, { + value: alicePlatformServicePostingFee, + }) + + const service1Data = await talentLayerServiceV1.services(1) + expect(service1Data.dataUri).to.be.equal(cid) + expect(service1Data.ownerId).to.be.equal(aliceTlId) + expect(service1Data.platformId).to.be.equal(alicePlatformId) + expect(service1Data.acceptedProposalId).to.be.equal(0) + expect(service1Data.transactionId).to.be.equal(0) + expect(service1Data.status).to.be.equal(0) + expect(tx) + .to.emit(talentLayerServiceV1, 'ServiceCreated') + .withArgs(1, aliceTlId, alicePlatformId, cid) + + await talentLayerServiceV1 + .connect(bob) + .createService(bobTlId, alicePlatformId, cid, fakeSignature, { + value: alicePlatformServicePostingFee, + }) + const service2Data = await talentLayerServiceV1.services(2) + + await talentLayerServiceV1 + .connect(carol) + .createService(carolTlId, alicePlatformId, cid, fakeSignature, { + value: alicePlatformServicePostingFee, + }) + const service3Data = await talentLayerServiceV1.services(3) + + await talentLayerServiceV1 + .connect(dave) + .createService(daveTlId, alicePlatformId, cid, fakeSignature, { + value: alicePlatformServicePostingFee, + }) + const service4Data = await talentLayerServiceV1.services(4) + + expect(service2Data.ownerId).to.be.equal(bobTlId) + expect(service3Data.ownerId).to.be.equal(carolTlId) + expect(service4Data.ownerId).to.be.equal(daveTlId) + }) + it('Bob and dave can create 2 MATIC proposals for service 1. ', async function () { + // Service1(alice): 2 proposals created, one validated MATIC, 25% released, 25% reimbursed + // ===> Need to release the rest + 100% reviews after upgrade + + // Proposal 1 + const tx = await talentLayerServiceV1 + .connect(bob) + .createProposal( + bobTlId, + 1, + ethers.constants.AddressZero, + rateAmount, + alicePlatformId, + cid, + proposalExpirationDate, + fakeSignature, + { + value: alicePlatformProposalPostingFee, + }, + ) + // Proposal 2 + await talentLayerServiceV1 + .connect(dave) + .createProposal( + daveTlId, + 1, + ethers.constants.AddressZero, + rateAmount, + alicePlatformId, + cid, + proposalExpirationDate, + fakeSignature, + { + value: alicePlatformProposalPostingFee, + }, + ) + }) + + it('Alice can validate proposal 1. 25% can be released & 25% reimbursed', async function () { + const totalAmount = getTotalTransactionValue( + talentLayerEscrowV1, + talentLayerPlatformID, + rateAmount, + ) + + // Transaction 1 + await talentLayerEscrowV1.connect(alice).createTransaction(1, bobTlId, metaEvidenceCid, cid, { + value: totalAmount, + }) + + await talentLayerEscrowV1.connect(bob).reimburse(bobTlId, 1, rateAmount / 4) + await talentLayerEscrowV1.connect(alice).release(aliceTlId, 1, rateAmount / 4) + + const transactionData = await talentLayerEscrowV1.connect(alice).getTransactionDetails(1) + expect(transactionData.amount).to.be.equal(rateAmount / 2) + }) + + it('Alice can create 1 ERC20 proposal for service 1. ', async function () { + // Service2(bob): 1 Proposal validated ERC20, 100% released, only buyer review sent + // ===> Need to 100% review + + // Proposal 3 + const tx = await talentLayerServiceV1 + .connect(alice) + .createProposal( + aliceTlId, + 2, + token.address, + rateAmount, + alicePlatformId, + cid, + proposalExpirationDate, + fakeSignature, + { + value: alicePlatformProposalPostingFee, + }, + ) + }) + + it('Bob can validate proposal 2 and release 100%', async function () { + const totalAmount = getTotalTransactionValue( + talentLayerEscrowV1, + talentLayerPlatformID, + rateAmount, + ) + await token.connect(bob).approve(talentLayerEscrowV1.address, totalAmount) + + // Transaction 2 + await talentLayerEscrowV1.connect(bob).createTransaction(2, aliceTlId, metaEvidenceCid, cid) + + await talentLayerEscrowV1.connect(bob).release(bobTlId, 2, rateAmount) + + const transactionData = await talentLayerEscrowV1.connect(alice).getTransactionDetails(2) + expect(transactionData.amount).to.be.equal(0) + }) + + it('Bob can review the transaction', async function () { + await talentLayerReview.connect(bob).mint(bobTlId, 2, cid, 4) + const review = await talentLayerReview.getReview(1) + expect(review.id).to.be.equal(1) + expect(review.ownerId).to.be.equal(aliceTlId) + expect(review.dataUri).to.be.equal(cid) + expect(review.serviceId).to.be.equal(2) + expect(review.rating).to.be.equal(4) + }) + + it('Alice & Dave can create 1 ERC20 proposal each for service 3. ', async function () { + // Service3(Carol): 2 proposals created ERC20, none validated + // ===> Need to validate one + 100% release + // Proposal 4 + await talentLayerServiceV1 + .connect(alice) + .createProposal( + aliceTlId, + 3, + token.address, + rateAmount, + alicePlatformId, + cid, + proposalExpirationDate, + fakeSignature, + { + value: alicePlatformProposalPostingFee, + }, + ) + await talentLayerServiceV1 + .connect(dave) + .createProposal( + daveTlId, + 3, + token.address, + rateAmount, + alicePlatformId, + cid, + proposalExpirationDate, + fakeSignature, + { + value: alicePlatformProposalPostingFee, + }, + ) + }) + // Service4(dave): Nothing + // ===> Need to create proposal after service creation, validate it, release 90%, reimburse 10% and review 100% + }) + + describe('Upgrade Service & Escrow contracts', async function () { + it('Upgrade should not alter data', async function () { + const service1DataBeforeUpgrade = await talentLayerServiceV1.services(1) + const proposalDataBeforeUpgrade = await talentLayerServiceV1.getProposal(1, 2) + talentLayerService = await upgradeServiceV1(talentLayerServiceV1.address) + talentLayerEscrow = await upgradeEscrowV1(talentLayerEscrowV1.address) + expect(talentLayerService.address).to.be.equal(talentLayerServiceV1.address) + expect(talentLayerEscrow.address).to.be.equal(talentLayerEscrowV1.address) + const service1DataAfterUpgrade = await talentLayerService.services(1) + const proposalDataAfterUpgrade = await talentLayerService.getProposal(1, 2) + expect(service1DataBeforeUpgrade.ownerId).to.be.equal(service1DataAfterUpgrade.ownerId) + expect(service1DataBeforeUpgrade.dataUri).to.be.equal(service1DataAfterUpgrade.dataUri) + expect(service1DataBeforeUpgrade.platformId).to.be.equal(service1DataAfterUpgrade.platformId) + expect(service1DataBeforeUpgrade.acceptedProposalId).to.be.equal( + service1DataAfterUpgrade.acceptedProposalId, + ) + expect(service1DataAfterUpgrade.rateToken).to.be.equal(ethers.constants.AddressZero) + + expect(proposalDataBeforeUpgrade.ownerId).to.be.equal(proposalDataAfterUpgrade.ownerId) + expect(proposalDataBeforeUpgrade.dataUri).to.be.equal(proposalDataAfterUpgrade.dataUri) + expect(proposalDataBeforeUpgrade.rateAmount).to.be.equal(proposalDataAfterUpgrade.rateAmount) + expect(proposalDataBeforeUpgrade.platformId).to.be.equal(proposalDataAfterUpgrade.platformId) + expect(proposalDataBeforeUpgrade.expirationDate).to.be.equal( + proposalDataAfterUpgrade.expirationDate, + ) + expect(proposalDataBeforeUpgrade.rateToken).to.be.equal(ethers.constants.AddressZero) + }) + describe('For Service1 => Check whether escrow balance can be released & reimbursed after an upgrade', async function () { + it('Alice can release 40% of the remaining escrow amount & Bob can reimburse 10% of the remaining escrow amount', async function () { + await talentLayerEscrow.connect(alice).release(aliceTlId, 1, (rateAmount * 4) / 10) + await talentLayerEscrow.connect(bob).reimburse(bobTlId, 1, rateAmount / 10) + const transactionData = await talentLayerEscrow.connect(alice).getTransactionDetails(1) + expect(transactionData.amount).to.be.equal(0) + // @dev: Legacy transactions should have this field equal to zero + expect(transactionData.totalAmount).to.be.equal(0) + }) + it('Bob & Alice can both review the service', async function () { + await talentLayerReview.connect(bob).mint(bobTlId, 1, cid, 5) + const bobReview = await talentLayerReview.getReview(2) + expect(bobReview.id).to.be.equal(2) + expect(bobReview.ownerId).to.be.equal(aliceTlId) + expect(bobReview.dataUri).to.be.equal(cid) + expect(bobReview.serviceId).to.be.equal(1) + expect(bobReview.rating).to.be.equal(5) + await talentLayerReview.connect(alice).mint(aliceTlId, 1, cid, 5) + const aliceReview = await talentLayerReview.getReview(3) + expect(aliceReview.id).to.be.equal(3) + expect(aliceReview.ownerId).to.be.equal(bobTlId) + expect(aliceReview.dataUri).to.be.equal(cid) + expect(aliceReview.serviceId).to.be.equal(1) + expect(aliceReview.rating).to.be.equal(5) + }) + }) + describe('For Service2 => Check whether a service can be reviewed after an upgrade', async function () { + it('Alice can review the service', async function () { + await talentLayerReview.connect(alice).mint(aliceTlId, 2, cid, 5) + const aliceReview = await talentLayerReview.getReview(4) + expect(aliceReview.id).to.be.equal(4) + expect(aliceReview.ownerId).to.be.equal(bobTlId) + expect(aliceReview.dataUri).to.be.equal(cid) + expect(aliceReview.serviceId).to.be.equal(2) + expect(aliceReview.rating).to.be.equal(5) + }) + }) + }) + describe('For Service3 => Check whether a proposal created before an upgrade can be validated after upgrade', async function () { + it("Even if Dave's proposal used ERC20 token, the validated one will be with MATIC", async function () { + const totalAmount = getTotalTransactionValue( + talentLayerEscrow, + talentLayerPlatformID, + rateAmount, + ) + // await token.connect(carol).approve(talentLayerEscrow.address, totalAmount) + + // Transaction 3 + await talentLayerEscrow.connect(carol).createTransaction(3, daveTlId, metaEvidenceCid, cid, { + value: totalAmount, + }) + const transactionData = await talentLayerEscrow.connect(carol).getTransactionDetails(3) + expect(transactionData.id).to.be.equal(3) + expect(transactionData.token).to.be.equal(ethers.constants.AddressZero) + }) + it('All funds can be released & reimbursed', async function () { + await talentLayerEscrow.connect(dave).reimburse(daveTlId, 3, (rateAmount * 9) / 10) + await talentLayerEscrow.connect(carol).release(carolTlId, 3, rateAmount / 10) + const transactionData = await talentLayerEscrow.connect(dave).getTransactionDetails(3) + expect(transactionData.amount).to.be.equal(0) + }) + }) + describe('For Service4 => Check whether a proposal can be created after the upgrade for a service created before', async function () { + it("Carol can create a proposal for Dave's service & both can release & reimburse", async function () { + await talentLayerService + .connect(carol) + .createProposal( + carolTlId, + 4, + rateAmount, + 1, + cid, + proposalExpirationDate, + fakeSignature, + 0, + { + value: alicePlatformProposalPostingFee, + }, + ) + + const totalAmount = getTotalTransactionValue( + talentLayerEscrow, + talentLayerPlatformID, + rateAmount, + ) + + // Transaction 4 + await talentLayerEscrow.connect(dave).createTransaction(4, carolTlId, metaEvidenceCid, cid, { + value: totalAmount, + }) + const transactionData = await talentLayerEscrow.connect(dave).getTransactionDetails(4) + expect(transactionData.id).to.be.equal(4) + expect(transactionData.token).to.be.equal(ethers.constants.AddressZero) + expect(transactionData.totalAmount).to.be.equal(rateAmount) + expect(transactionData.sender).to.be.equal(dave.address) + expect(transactionData.receiver).to.be.equal(carol.address) + expect(transactionData.totalAmount).to.be.equal(rateAmount) + expect(transactionData.referrerId).to.be.equal(0) + }) + it('All funds can be released & reimbursed', async function () { + await talentLayerEscrow.connect(dave).release(daveTlId, 4, (rateAmount * 9) / 10) + await talentLayerEscrow.connect(carol).reimburse(carolTlId, 4, rateAmount / 10) + const transactionData = await talentLayerEscrow.connect(dave).getTransactionDetails(4) + expect(transactionData.amount).to.be.equal(0) + }) + }) +}) + +const getTotalTransactionValue = async ( + talentLayerEscrow: Contract, + talentLayerPlatformID: Contract, + rateAmount: number, +): Promise => { + const protocolEscrowFeeRate = await talentLayerEscrow.protocolEscrowFeeRate() + const originServiceFeeRate = await talentLayerPlatformID.getOriginServiceFeeRate(alicePlatformId) + const originValidatedProposalFeeRate = + await talentLayerPlatformID.getOriginValidatedProposalFeeRate(alicePlatformId) + + return ( + rateAmount + + (rateAmount * (protocolEscrowFeeRate + originValidatedProposalFeeRate + originServiceFeeRate)) / + FEE_DIVIDER + ) +} diff --git a/test/manual/audit.ts b/test/manual/audit.ts index 0f75f7d2..1b9572d1 100644 --- a/test/manual/audit.ts +++ b/test/manual/audit.ts @@ -117,7 +117,7 @@ describe('Audit test', function () { const signature = await getSignatureForService(carol, aliceTlId.toNumber(), 0, cid) await talentLayerService .connect(alice) - .createService(aliceTlId, carolPlatformId, cid, signature, { + .createService(aliceTlId, carolPlatformId, cid, signature, ethers.constants.AddressZero, { value: 0, }) @@ -125,16 +125,7 @@ describe('Audit test', function () { const signature2 = await getSignatureForProposal(carol, bobTlId.toNumber(), 0, cid) const tx = talentLayerService .connect(bob) - .createProposal( - bobTlId, - 99, - ethers.constants.AddressZero, - 1000, - carolPlatformId, - cid, - proposalExpirationDate, - signature2, - ) + .createProposal(bobTlId, 99, 1000, carolPlatformId, cid, proposalExpirationDate, signature2) await expect(tx).to.revertedWith('Service not exist') }) }) diff --git a/test/manual/loadTest.ts b/test/manual/loadTest.ts index 86fb7b3e..2eea263d 100644 --- a/test/manual/loadTest.ts +++ b/test/manual/loadTest.ts @@ -91,7 +91,13 @@ describe('Load test', function () { await expect( await talentLayerService .connect(signers[signerIndex]) - .createService(talentLayerId, platformId, cid, signature), + .createService( + talentLayerId, + platformId, + cid, + signature, + ethers.constants.AddressZero, + ), ).to.emit(talentLayerService, 'ServiceCreated') } } @@ -123,7 +129,6 @@ describe('Load test', function () { .createProposal( talentLayerId, serviceId, - ethers.constants.AddressZero, VALUE, platformId, cid, diff --git a/test/utils/deploy.ts b/test/utils/deploy.ts index 1ee36842..3eab7a6a 100644 --- a/test/utils/deploy.ts +++ b/test/utils/deploy.ts @@ -5,9 +5,11 @@ import { SimpleERC20, TalentLayerArbitrator, TalentLayerEscrow, + TalentLayerEscrowV1, TalentLayerID, TalentLayerPlatformID, TalentLayerReview, + TalentLayerServiceV1, } from '../../typechain-types' /** @@ -40,7 +42,7 @@ export async function deploy( let talentLayerID = await upgrades.deployProxy(TalentLayerID, talentLayerIDArgs) if (applyUpgrade) { - const TalentLayerIDV2 = await ethers.getContractFactory('TalentLayerIDV2') + const TalentLayerIDV2 = await ethers.getContractFactory('TalentLayerID') talentLayerID = await upgrades.upgradeProxy(talentLayerID.address, TalentLayerIDV2) } @@ -50,7 +52,7 @@ export async function deploy( talentLayerID.address, talentLayerPlatformID.address, ] - const talentLayerService = await upgrades.deployProxy(TalentLayerService, talentLayerServiceArgs) + let talentLayerService = await upgrades.deployProxy(TalentLayerService, talentLayerServiceArgs) // Deploy TalentLayerArbitrator const TalentLayerArbitrator = await ethers.getContractFactory('TalentLayerArbitrator') @@ -64,7 +66,7 @@ export async function deploy( talentLayerPlatformID.address, networkConfig.multisigAddressList.fee, ] - const talentLayerEscrow = await upgrades.deployProxy(TalentLayerEscrow, TalentLayerEscrowArgs) + let talentLayerEscrow = await upgrades.deployProxy(TalentLayerEscrow, TalentLayerEscrowArgs) const escrowRole = await talentLayerService.ESCROW_ROLE() await talentLayerService.grantRole(escrowRole, talentLayerEscrow.address) @@ -80,6 +82,16 @@ export async function deploy( const SimpleERC20 = await ethers.getContractFactory('SimpleERC20') const simpleERC20 = await SimpleERC20.deploy() + if (applyUpgrade) { + const talentLayerEscrowV2 = await ethers.getContractFactory('TalentLayerEscrowV2') + talentLayerEscrow = await upgrades.upgradeProxy(talentLayerEscrow.address, talentLayerEscrowV2) + const talentLayerServiceV2 = await ethers.getContractFactory('TalentLayerServiceV2') + talentLayerService = await upgrades.upgradeProxy( + talentLayerService.address, + talentLayerServiceV2, + ) + } + return [ talentLayerID as TalentLayerID, talentLayerPlatformID as TalentLayerPlatformID, @@ -90,3 +102,91 @@ export async function deploy( simpleERC20, ] } +export async function deployForV1(): Promise< + [ + TalentLayerID, + TalentLayerPlatformID, + TalentLayerEscrowV1, + TalentLayerArbitrator, + TalentLayerServiceV1, + TalentLayerReview, + SimpleERC20, + ] +> { + const chainId = network.config.chainId ? network.config.chainId : Network.LOCAL + const networkConfig: NetworkConfig = getConfig(chainId) + + // Deploy PlatformId + const TalentLayerPlatformID = await ethers.getContractFactory('TalentLayerPlatformID') + const talentLayerPlatformID = await upgrades.deployProxy(TalentLayerPlatformID) + + // Deploy TalentLayerID + const TalentLayerID = await ethers.getContractFactory('TalentLayerID') + const talentLayerIDArgs: [string] = [talentLayerPlatformID.address] + const talentLayerID = await upgrades.deployProxy(TalentLayerID, talentLayerIDArgs) + + // Deploy TalentLayerService + const TalentLayerService = await ethers.getContractFactory('TalentLayerServiceV1') + const talentLayerServiceArgs: [string, string] = [ + talentLayerID.address, + talentLayerPlatformID.address, + ] + const talentLayerService = await upgrades.deployProxy(TalentLayerService, talentLayerServiceArgs) + + // Deploy TalentLayerArbitrator + const TalentLayerArbitrator = await ethers.getContractFactory('TalentLayerArbitrator') + const talentLayerArbitrator = await TalentLayerArbitrator.deploy(talentLayerPlatformID.address) + + // Deploy TalentLayerEscrow and escrow role on TalentLayerService + const TalentLayerEscrow = await ethers.getContractFactory('TalentLayerEscrowV1') + const TalentLayerEscrowArgs: [string, string, string, string | undefined] = [ + talentLayerService.address, + talentLayerID.address, + talentLayerPlatformID.address, + networkConfig.multisigAddressList.fee, + ] + const talentLayerEscrow = await upgrades.deployProxy(TalentLayerEscrow, TalentLayerEscrowArgs) + const escrowRole = await talentLayerService.ESCROW_ROLE() + await talentLayerService.grantRole(escrowRole, talentLayerEscrow.address) + + // Deploy TalentLayerReview + const TalentLayerReview = await ethers.getContractFactory('TalentLayerReview') + const talentLayerReviewArgs: [string, string] = [ + talentLayerID.address, + talentLayerService.address, + ] + const talentLayerReview = await upgrades.deployProxy(TalentLayerReview, talentLayerReviewArgs) + + // Deploy SimpleERC20 Token + const SimpleERC20 = await ethers.getContractFactory('SimpleERC20') + const simpleERC20 = await SimpleERC20.deploy() + + return [ + talentLayerID as TalentLayerID, + talentLayerPlatformID as TalentLayerPlatformID, + talentLayerEscrow as TalentLayerEscrowV1, + talentLayerArbitrator, + talentLayerService as TalentLayerServiceV1, + talentLayerReview as TalentLayerReview, + simpleERC20, + ] +} + +export const upgradeServiceV1 = async (talentLayerServiceAddress: string) => { + const talentLayerServiceUpgrade = await ethers.getContractFactory('TalentLayerService') + const talentLayerService = await upgrades.upgradeProxy( + talentLayerServiceAddress, + talentLayerServiceUpgrade, + ) + + return talentLayerService as TalentLayerService +} + +export const upgradeEscrowV1 = async (talentLayerEscrowAddress: string) => { + const talentLayerEscrowUpgrade = await ethers.getContractFactory('TalentLayerEscrow') + const talentLayerEscrow = await upgrades.upgradeProxy( + talentLayerEscrowAddress, + talentLayerEscrowUpgrade, + ) + return talentLayerEscrow as TalentLayerEscrow +}