From e9afd464150f1d4f371acda64d5d2049003f5db8 Mon Sep 17 00:00:00 2001 From: Ruben Dinis Date: Tue, 15 Oct 2024 17:53:12 +0100 Subject: [PATCH 01/74] Setup deploy scripts --- scripts/passport/deployScorer.ts | 9 +- scripts/talent/deployBuilderdropUnlocks.ts | 102 +++++++++++++++++++++ scripts/talent/deployPurchasesUnlocks.ts | 102 +++++++++++++++++++++ scripts/talent/deployTalentRewardClaim.ts | 54 +++-------- 4 files changed, 222 insertions(+), 45 deletions(-) create mode 100644 scripts/talent/deployBuilderdropUnlocks.ts create mode 100644 scripts/talent/deployPurchasesUnlocks.ts diff --git a/scripts/passport/deployScorer.ts b/scripts/passport/deployScorer.ts index c56976a2..7dcd9886 100644 --- a/scripts/passport/deployScorer.ts +++ b/scripts/passport/deployScorer.ts @@ -4,6 +4,9 @@ import { deployPassportBuilderScore, deploySmartBuilderScore } from "../shared"; const PASSPORT_MAINNET = "0xb477A9BD2547ad61f4Ac22113172Dd909E5B2331"; const PASSPORT_TESTNET = "0xa600b3356c1440B6D6e57b0B7862dC3dFB66bc43"; +const FEE_RECEIVER_MAINNET = "0xC925bD0E839E8e22A7DDEbe7f4C21b187deeC358"; +const FEE_RECEIVER_TESTNET = "0x08BC8a92e5C99755C675A21BC4FcfFb59E0A9508"; + async function main() { console.log(`Deploying passport builder score at ${network.name}`); @@ -11,15 +14,15 @@ async function main() { console.log(`Admin will be ${admin.address}`); - const builderScore = await deployPassportBuilderScore(PASSPORT_MAINNET, admin.address); + const builderScore = await deployPassportBuilderScore(PASSPORT_TESTNET, admin.address); console.log(`Scorer Address: ${builderScore.address}`); console.log(`Scorer owner: ${await builderScore.owner()}`); const smartBuilderScore = await deploySmartBuilderScore( admin.address, - PASSPORT_MAINNET, - "0xC925bD0E839E8e22A7DDEbe7f4C21b187deeC358", + PASSPORT_TESTNET, + FEE_RECEIVER_TESTNET, builderScore.address ); diff --git a/scripts/talent/deployBuilderdropUnlocks.ts b/scripts/talent/deployBuilderdropUnlocks.ts new file mode 100644 index 00000000..7428efd1 --- /dev/null +++ b/scripts/talent/deployBuilderdropUnlocks.ts @@ -0,0 +1,102 @@ +import { ethers, network } from "hardhat"; +import { deployTalentTGEUnlock } from "../shared"; +import fs from "fs"; + +import { BigNumberish } from "ethers"; + +import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; + +import distributionSetup from "../data/summerBuilderdrop.json"; +import { createClient } from "@supabase/supabase-js"; + +const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; +const TALENT_TOKEN_ADDRESS_MAINNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; + +const VESTING_CATEGORY = "summer_builderdrop" + +type BalanceMap = { + [key: string]: BigNumberish; +}; + +function generateMerkleTree(snapshot: BalanceMap): StandardMerkleTree<(string | BigNumberish)[]> { + const leaves = Object.keys(snapshot).map((address) => [address, snapshot[address]]); + + return StandardMerkleTree.of(leaves, ["address", "uint256"]); +} + +async function main() { + console.log(`Deploying TGE Unlocks at ${network.name}`); + + const [admin] = await ethers.getSigners(); + + console.log(`Admin will be ${admin.address}`); + + if(!process.env.PUBLIC_SUPABASE_URL) { + console.error("Missing PUBLIC_SUPABASE_URL"); + return 0; + } + + if(!process.env.PUBLIC_SUPABASE_ANON_KEY) { + console.error("Missing PUBLIC_SUPABASE_ANON_KEY"); + return 0; + } + + const allResults = distributionSetup as { amount: string; wallet: string }[]; + + const merkleBase = allResults.reduce((acc, { wallet, amount }) => { + acc[wallet.toLowerCase()] = ethers.utils.parseEther(amount).toBigInt(); + return acc; + }, {} as Record); + + const merkleTree = generateMerkleTree(merkleBase); + console.log("Generated merkle trees: ", merkleTree.root); + const tgeUnlockDistribution = await deployTalentTGEUnlock(TALENT_TOKEN_ADDRESS_TESTNET, admin.address, merkleTree.root); + + console.log(`TGE Unlock distribution deployed at ${tgeUnlockDistribution.address}`); + const proofList = allResults.map(({ wallet, amount }) => { + const value = ethers.utils.parseEther(amount); + const proof = merkleTree.getProof([wallet.toLowerCase(), value]); + return { + wallet, + value, + proof, + }; + }); + + console.log("Writing proofs to file"); + fs.writeFileSync( + `./data/${VESTING_CATEGORY}-proofs.json`, + JSON.stringify(proofList, (key, value) => (typeof value === "bigint" ? value.toString() : value)) + ); + + console.log("Uploading proofs to database"); + + const supabase = createClient(process.env.PUBLIC_SUPABASE_URL, process.env.PUBLIC_SUPABASE_ANON_KEY) + + const proofsCount = proofList.length + for (let i = 0; i < proofsCount; i++) { + const element = proofList[i] + + console.log(`Uploading ${i + 1}/${proofsCount}: ${element.wallet}`) + + const { error } = await supabase + .from("distributions") + .update({ proof: element.proof }) + .eq("wallet", element.wallet) + .eq("vesting_category", VESTING_CATEGORY) + + if(error) { + console.error(error); + } + } + + + console.log("Done"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/talent/deployPurchasesUnlocks.ts b/scripts/talent/deployPurchasesUnlocks.ts new file mode 100644 index 00000000..0ca28581 --- /dev/null +++ b/scripts/talent/deployPurchasesUnlocks.ts @@ -0,0 +1,102 @@ +import { ethers, network } from "hardhat"; +import { deployTalentTGEUnlock } from "../shared"; +import fs from "fs"; + +import { BigNumberish } from "ethers"; + +import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; + +import distributionSetup from "../data/inAppPurchases.json"; +import { createClient } from "@supabase/supabase-js"; + +const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; +const TALENT_TOKEN_ADDRESS_MAINNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; + +const VESTING_CATEGORY = "ecosystem_incentives_02" + +type BalanceMap = { + [key: string]: BigNumberish; +}; + +function generateMerkleTree(snapshot: BalanceMap): StandardMerkleTree<(string | BigNumberish)[]> { + const leaves = Object.keys(snapshot).map((address) => [address, snapshot[address]]); + + return StandardMerkleTree.of(leaves, ["address", "uint256"]); +} + +async function main() { + console.log(`Deploying TGE Unlocks at ${network.name}`); + + const [admin] = await ethers.getSigners(); + + console.log(`Admin will be ${admin.address}`); + + if(!process.env.PUBLIC_SUPABASE_URL) { + console.error("Missing PUBLIC_SUPABASE_URL"); + return 0; + } + + if(!process.env.PUBLIC_SUPABASE_ANON_KEY) { + console.error("Missing PUBLIC_SUPABASE_ANON_KEY"); + return 0; + } + + const allResults = distributionSetup as { amount: string; wallet: string }[]; + + const merkleBase = allResults.reduce((acc, { wallet, amount }) => { + acc[wallet.toLowerCase()] = ethers.utils.parseEther(amount).toBigInt(); + return acc; + }, {} as Record); + + const merkleTree = generateMerkleTree(merkleBase); + console.log("Generated merkle trees: ", merkleTree.root); + const tgeUnlockDistribution = await deployTalentTGEUnlock(TALENT_TOKEN_ADDRESS_TESTNET, admin.address, merkleTree.root); + + console.log(`TGE Unlock distribution deployed at ${tgeUnlockDistribution.address}`); + const proofList = allResults.map(({ wallet, amount }) => { + const value = ethers.utils.parseEther(amount); + const proof = merkleTree.getProof([wallet.toLowerCase(), value]); + return { + wallet, + value, + proof, + }; + }); + + console.log("Writing proofs to file"); + fs.writeFileSync( + "./data/inAppPutchasesProofs.json", + JSON.stringify(proofList, (key, value) => (typeof value === "bigint" ? value.toString() : value)) + ); + + console.log("Uploading proofs to database"); + + const supabase = createClient(process.env.PUBLIC_SUPABASE_URL, process.env.PUBLIC_SUPABASE_ANON_KEY) + + const proofsCount = proofList.length + for (let i = 0; i < proofsCount; i++) { + const element = proofList[i] + + console.log(`Uploading ${i + 1}/${proofsCount}: ${element.wallet}`) + + const { error } = await supabase + .from("distributions") + .update({ proof: element.proof }) + .eq("wallet", element.wallet) + .eq("vesting_category", VESTING_CATEGORY) + + if(error) { + console.error(error); + } + } + + + console.log("Done"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/talent/deployTalentRewardClaim.ts b/scripts/talent/deployTalentRewardClaim.ts index 0b73dfc9..ae1e0ed1 100644 --- a/scripts/talent/deployTalentRewardClaim.ts +++ b/scripts/talent/deployTalentRewardClaim.ts @@ -3,22 +3,13 @@ import { deployTalentRewardClaim } from "../shared"; import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; import fs from "fs"; import { createClient } from '@supabase/supabase-js' +import rewardDistributions from "../data/rewardsDistribution.json"; -import * as TalentRewardClaim from "../../artifacts/contracts/talent/TalentRewardClaim.sol/TalentRewardClaim.json"; +const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; +const TALENT_TOKEN_ADDRESS_MAINNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; -// @TODO: add all safes and addresses that need to receive the tokens -const wallets = [ - "0xf9342d70a2a6eb46afd7b81138dee01d73b2e419", - "0xc8b74c37bd25e6ca8cb6ddf2e01058c45d341182", - "0x33041027dd8f4dc82b6e825fb37adf8f15d44053", - "0x58a35cf59d5c630c057af008a78bc67cdc2ec094", - "0x923b6bfc8cb0d9a57716a1340f7b86e8b678ecea", - "0xf924efc8830bfa1029fa0cd7a51901a5ec03de3d", - "0xa081e1da16133bb4ebc7aab1a9b0588a48d15138", - "0xe3b35ff40263385159f5705ece0223ea81730692" - ]; - -// 2471833440000000000000000 +const BUILDER_SCORE_ADDRESS_TESTNET = "0xe6b4388B1ECE6863349Ff41D79C408D9E211E59a" +const BUILDER_SCORE_ADDRESS_MAINNET = "0xe6b4388B1ECE6863349Ff41D79C408D9E211E59a" async function main() { console.log(`Deploying Talent Reward Claim at ${network.name}`); @@ -35,32 +26,13 @@ async function main() { const supabase = createClient(process.env.PUBLIC_SUPABASE_URL, process.env.PUBLIC_SUPABASE_ANON_KEY) - // Consider limit of 1000 rows; sort - const { data, error } = await supabase - .from("distributions") - .select("wallet, amount") - .eq("vesting_category", "ecosystem_incentives_03") - .in("wallet", wallets) - - console.log(data); - - if(error) { - console.error(error); - return 0; - } - - if(!data || data.length == 0) { - console.error("No data to process"); - return 0; - } - const [admin] = await ethers.getSigners(); console.log("Calculating merkle tree"); - const leaves = data.map((leave) => [ - leave.wallet, - ethers.utils.parseEther(leave.amount.toFixed(2).toString()), + const leaves = rewardDistributions.map((distribution) => [ + distribution.wallet, + ethers.utils.parseEther(distribution.amount), ]); console.log("Leaves", leaves); @@ -102,15 +74,13 @@ async function main() { console.log("Deploying..."); console.log(`Admin will be ${admin.address}`); - const talentAddress = "" // Talent Token Address - const builderScoreAddress = "" // Builder Score Contract Address - const holdingWalletAddress = "" // Holding Wallet Address + const holdingWalletAddress = admin.address // Holding Wallet Address - console.log(`Contract init args: ${talentAddress} ${builderScoreAddress} ${holdingWalletAddress} ${admin.address} ${merkleTree.root}`) + console.log(`Contract init args: ${TALENT_TOKEN_ADDRESS_TESTNET} ${BUILDER_SCORE_ADDRESS_TESTNET} ${holdingWalletAddress} ${admin.address} ${merkleTree.root}`) const talentRewardClaim = await deployTalentRewardClaim( - talentAddress, - builderScoreAddress, + TALENT_TOKEN_ADDRESS_TESTNET, + BUILDER_SCORE_ADDRESS_TESTNET, holdingWalletAddress, admin.address, merkleTree.root From 8418b4ae542732c756fca9f7ef8c45b40176c90e Mon Sep 17 00:00:00 2001 From: Ruben Dinis Date: Fri, 18 Oct 2024 12:28:01 +0100 Subject: [PATCH 02/74] Add generation scripts for TGE unlocks --- scripts/shared/index.ts | 2 +- scripts/talent/deployBuilderdropUnlocks.ts | 67 +++++++++------------- scripts/talent/deployPurchasesUnlocks.ts | 9 ++- scripts/talent/deployTalentRewardClaim.ts | 46 ++++++++------- 4 files changed, 58 insertions(+), 66 deletions(-) diff --git a/scripts/shared/index.ts b/scripts/shared/index.ts index 3dc49be1..0160ced3 100644 --- a/scripts/shared/index.ts +++ b/scripts/shared/index.ts @@ -104,7 +104,7 @@ export async function deployTalentTGEUnlock( ): Promise { const talentTGEUnlockContract = await ethers.getContractFactory("TalentTGEUnlock"); - const deployedTGEUnlock = await talentTGEUnlockContract.deploy(token, owner, merkleTreeRoot); + const deployedTGEUnlock = await talentTGEUnlockContract.deploy(token, merkleTreeRoot, owner); await deployedTGEUnlock.deployed(); return deployedTGEUnlock as TalentTGEUnlock; } diff --git a/scripts/talent/deployBuilderdropUnlocks.ts b/scripts/talent/deployBuilderdropUnlocks.ts index 7428efd1..cc71067b 100644 --- a/scripts/talent/deployBuilderdropUnlocks.ts +++ b/scripts/talent/deployBuilderdropUnlocks.ts @@ -10,7 +10,7 @@ import distributionSetup from "../data/summerBuilderdrop.json"; import { createClient } from "@supabase/supabase-js"; const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; -const TALENT_TOKEN_ADDRESS_MAINNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; +const TALENT_TOKEN_ADDRESS_MAINNET = "0x9a33406165f562E16C3abD82fd1185482E01b49a"; const VESTING_CATEGORY = "summer_builderdrop" @@ -31,65 +31,52 @@ async function main() { console.log(`Admin will be ${admin.address}`); - if(!process.env.PUBLIC_SUPABASE_URL) { - console.error("Missing PUBLIC_SUPABASE_URL"); - return 0; - } - - if(!process.env.PUBLIC_SUPABASE_ANON_KEY) { - console.error("Missing PUBLIC_SUPABASE_ANON_KEY"); - return 0; - } - const allResults = distributionSetup as { amount: string; wallet: string }[]; + console.log("Generate merkle tree"); + const merkleBase = allResults.reduce((acc, { wallet, amount }) => { acc[wallet.toLowerCase()] = ethers.utils.parseEther(amount).toBigInt(); return acc; }, {} as Record); const merkleTree = generateMerkleTree(merkleBase); - console.log("Generated merkle trees: ", merkleTree.root); - const tgeUnlockDistribution = await deployTalentTGEUnlock(TALENT_TOKEN_ADDRESS_TESTNET, admin.address, merkleTree.root); - console.log(`TGE Unlock distribution deployed at ${tgeUnlockDistribution.address}`); + console.log("Generate proof list"); + + let index = 0; + let fileIndex = 0; + fs.writeFileSync(`scripts/data/builderdrop-proofs-${fileIndex}.json`, "[") + const proofList = allResults.map(({ wallet, amount }) => { - const value = ethers.utils.parseEther(amount); + const value = ethers.utils.parseEther(amount).toBigInt(); const proof = merkleTree.getProof([wallet.toLowerCase(), value]); + + index += 1; + + if(index % 100000 == 0) { + fs.appendFileSync(`scripts/data/builderdrop-proofs-${fileIndex}.json`, "]") + fileIndex +=1 + fs.writeFileSync(`scripts/data/builderdrop-proofs-${fileIndex}.json`, "[") + } + + const message = JSON.stringify({wallet, proof}) + + fs.appendFileSync(`scripts/data/builderdrop-proofs-${fileIndex}.json`, `${message},\n`) + return { wallet, - value, proof, }; }); - console.log("Writing proofs to file"); - fs.writeFileSync( - `./data/${VESTING_CATEGORY}-proofs.json`, - JSON.stringify(proofList, (key, value) => (typeof value === "bigint" ? value.toString() : value)) - ); - - console.log("Uploading proofs to database"); - - const supabase = createClient(process.env.PUBLIC_SUPABASE_URL, process.env.PUBLIC_SUPABASE_ANON_KEY) - - const proofsCount = proofList.length - for (let i = 0; i < proofsCount; i++) { - const element = proofList[i] + fs.appendFileSync(`scripts/data/builderdrop-proofs-${fileIndex}.json`, "]") - console.log(`Uploading ${i + 1}/${proofsCount}: ${element.wallet}`) + console.log(`Contract init args: ${TALENT_TOKEN_ADDRESS_TESTNET} ${merkleTree.root} ${admin.address}`) - const { error } = await supabase - .from("distributions") - .update({ proof: element.proof }) - .eq("wallet", element.wallet) - .eq("vesting_category", VESTING_CATEGORY) - - if(error) { - console.error(error); - } - } + const tgeUnlockDistribution = await deployTalentTGEUnlock(TALENT_TOKEN_ADDRESS_TESTNET, admin.address, merkleTree.root); + console.log(`TGE Unlock distribution deployed at ${tgeUnlockDistribution.address}`); console.log("Done"); } diff --git a/scripts/talent/deployPurchasesUnlocks.ts b/scripts/talent/deployPurchasesUnlocks.ts index 0ca28581..4760ca89 100644 --- a/scripts/talent/deployPurchasesUnlocks.ts +++ b/scripts/talent/deployPurchasesUnlocks.ts @@ -10,7 +10,7 @@ import distributionSetup from "../data/inAppPurchases.json"; import { createClient } from "@supabase/supabase-js"; const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; -const TALENT_TOKEN_ADDRESS_MAINNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; +const TALENT_TOKEN_ADDRESS_MAINNET = "0x9a33406165f562E16C3abD82fd1185482E01b49a"; const VESTING_CATEGORY = "ecosystem_incentives_02" @@ -43,13 +43,16 @@ async function main() { const allResults = distributionSetup as { amount: string; wallet: string }[]; + console.log("Generate merkle tree") + const merkleBase = allResults.reduce((acc, { wallet, amount }) => { acc[wallet.toLowerCase()] = ethers.utils.parseEther(amount).toBigInt(); return acc; }, {} as Record); const merkleTree = generateMerkleTree(merkleBase); - console.log("Generated merkle trees: ", merkleTree.root); + + console.log(`Contract init args: ${TALENT_TOKEN_ADDRESS_TESTNET} ${merkleTree.root} ${admin.address}`) const tgeUnlockDistribution = await deployTalentTGEUnlock(TALENT_TOKEN_ADDRESS_TESTNET, admin.address, merkleTree.root); console.log(`TGE Unlock distribution deployed at ${tgeUnlockDistribution.address}`); @@ -65,7 +68,7 @@ async function main() { console.log("Writing proofs to file"); fs.writeFileSync( - "./data/inAppPutchasesProofs.json", + `scripts/data/${VESTING_CATEGORY}-proofs.json`, JSON.stringify(proofList, (key, value) => (typeof value === "bigint" ? value.toString() : value)) ); diff --git a/scripts/talent/deployTalentRewardClaim.ts b/scripts/talent/deployTalentRewardClaim.ts index ae1e0ed1..d5efba93 100644 --- a/scripts/talent/deployTalentRewardClaim.ts +++ b/scripts/talent/deployTalentRewardClaim.ts @@ -6,10 +6,12 @@ import { createClient } from '@supabase/supabase-js' import rewardDistributions from "../data/rewardsDistribution.json"; const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; -const TALENT_TOKEN_ADDRESS_MAINNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; +const TALENT_TOKEN_ADDRESS_MAINNET = "0x9a33406165f562E16C3abD82fd1185482E01b49a"; -const BUILDER_SCORE_ADDRESS_TESTNET = "0xe6b4388B1ECE6863349Ff41D79C408D9E211E59a" -const BUILDER_SCORE_ADDRESS_MAINNET = "0xe6b4388B1ECE6863349Ff41D79C408D9E211E59a" +const BUILDER_SCORE_ADDRESS_TESTNET = "0x5f3aA689C4DCBAe505E6F6c8548DbD9b908bA71d" +const BUILDER_SCORE_ADDRESS_MAINNET = "0xBBFeDA7c4d8d9Df752542b03CdD715F790B32D0B" + +const VESTING_CATEGORY = "ecosystem_incentives_03" async function main() { console.log(`Deploying Talent Reward Claim at ${network.name}`); @@ -41,12 +43,29 @@ async function main() { console.log("Dumping tree to file"); - fs.writeFileSync("./merkeTreeForRewardClaiming.json", JSON.stringify(merkleTree.dump(), (key, value) => + fs.writeFileSync(`scripts/data/${VESTING_CATEGORY}-proofs.json`, JSON.stringify(merkleTree.dump(), (key, value) => typeof value === 'bigint' ? value.toString() : value )); + console.log("Deploying..."); + console.log(`Admin will be ${admin.address}`); + + const holdingWalletAddress = admin.address // Holding Wallet Address + + console.log(`Contract init args: ${TALENT_TOKEN_ADDRESS_TESTNET} ${BUILDER_SCORE_ADDRESS_TESTNET} ${holdingWalletAddress} ${admin.address} ${merkleTree.root}`) + + const talentRewardClaim = await deployTalentRewardClaim( + TALENT_TOKEN_ADDRESS_TESTNET, + BUILDER_SCORE_ADDRESS_TESTNET, + holdingWalletAddress, + admin.address, + merkleTree.root + ); + + console.log(`Talent Reward Claim Address: ${talentRewardClaim.address}`); + console.log("Uploading proofs to database"); const walletProof = leaves.map((leave) => [ @@ -64,30 +83,13 @@ async function main() { .from("distributions") .update({ proof: element[1] }) .eq("wallet", element[0]) - .eq("vesting_category", "ecosystem_incentives_03") + .eq("vesting_category", VESTING_CATEGORY) if(error) { console.error(error); } } - console.log("Deploying..."); - console.log(`Admin will be ${admin.address}`); - - const holdingWalletAddress = admin.address // Holding Wallet Address - - console.log(`Contract init args: ${TALENT_TOKEN_ADDRESS_TESTNET} ${BUILDER_SCORE_ADDRESS_TESTNET} ${holdingWalletAddress} ${admin.address} ${merkleTree.root}`) - - const talentRewardClaim = await deployTalentRewardClaim( - TALENT_TOKEN_ADDRESS_TESTNET, - BUILDER_SCORE_ADDRESS_TESTNET, - holdingWalletAddress, - admin.address, - merkleTree.root - ); - - console.log(`Talent Reward Claim Address: ${talentRewardClaim.address}`); - console.log("Done"); } From 1e64032b1350f3d32ac83e1df59b3c11dd3655b2 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Fri, 25 Oct 2024 16:11:45 +0100 Subject: [PATCH 03/74] Apply linting to unlock scripts --- scripts/talent/deployPurchasesUnlocks.ts | 35 +++++++++++++----------- test/shared/artifacts.ts | 2 ++ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/scripts/talent/deployPurchasesUnlocks.ts b/scripts/talent/deployPurchasesUnlocks.ts index 4760ca89..4bb46ddd 100644 --- a/scripts/talent/deployPurchasesUnlocks.ts +++ b/scripts/talent/deployPurchasesUnlocks.ts @@ -6,13 +6,13 @@ import { BigNumberish } from "ethers"; import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; -import distributionSetup from "../data/inAppPurchases.json"; +import distributionSetup from "../data/ecosystem-incentives-02.json"; import { createClient } from "@supabase/supabase-js"; -const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; -const TALENT_TOKEN_ADDRESS_MAINNET = "0x9a33406165f562E16C3abD82fd1185482E01b49a"; +const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; +const TALENT_TOKEN_ADDRESS_MAINNET = "0x9a33406165f562E16C3abD82fd1185482E01b49a"; -const VESTING_CATEGORY = "ecosystem_incentives_02" +const VESTING_CATEGORY = "ecosystem_incentives_02"; type BalanceMap = { [key: string]: BigNumberish; @@ -31,19 +31,19 @@ async function main() { console.log(`Admin will be ${admin.address}`); - if(!process.env.PUBLIC_SUPABASE_URL) { + if (!process.env.PUBLIC_SUPABASE_URL) { console.error("Missing PUBLIC_SUPABASE_URL"); return 0; } - if(!process.env.PUBLIC_SUPABASE_ANON_KEY) { + if (!process.env.PUBLIC_SUPABASE_ANON_KEY) { console.error("Missing PUBLIC_SUPABASE_ANON_KEY"); return 0; } const allResults = distributionSetup as { amount: string; wallet: string }[]; - console.log("Generate merkle tree") + console.log("Generate merkle tree"); const merkleBase = allResults.reduce((acc, { wallet, amount }) => { acc[wallet.toLowerCase()] = ethers.utils.parseEther(amount).toBigInt(); @@ -52,8 +52,12 @@ async function main() { const merkleTree = generateMerkleTree(merkleBase); - console.log(`Contract init args: ${TALENT_TOKEN_ADDRESS_TESTNET} ${merkleTree.root} ${admin.address}`) - const tgeUnlockDistribution = await deployTalentTGEUnlock(TALENT_TOKEN_ADDRESS_TESTNET, admin.address, merkleTree.root); + console.log(`Contract init args: ${TALENT_TOKEN_ADDRESS_MAINNET} ${merkleTree.root} ${admin.address}`); + const tgeUnlockDistribution = await deployTalentTGEUnlock( + TALENT_TOKEN_ADDRESS_MAINNET, + admin.address, + merkleTree.root + ); console.log(`TGE Unlock distribution deployed at ${tgeUnlockDistribution.address}`); const proofList = allResults.map(({ wallet, amount }) => { @@ -74,26 +78,25 @@ async function main() { console.log("Uploading proofs to database"); - const supabase = createClient(process.env.PUBLIC_SUPABASE_URL, process.env.PUBLIC_SUPABASE_ANON_KEY) + const supabase = createClient(process.env.PUBLIC_SUPABASE_URL, process.env.PUBLIC_SUPABASE_ANON_KEY); - const proofsCount = proofList.length + const proofsCount = proofList.length; for (let i = 0; i < proofsCount; i++) { - const element = proofList[i] + const element = proofList[i]; - console.log(`Uploading ${i + 1}/${proofsCount}: ${element.wallet}`) + console.log(`Uploading ${i + 1}/${proofsCount}: ${element.wallet}`); const { error } = await supabase .from("distributions") .update({ proof: element.proof }) .eq("wallet", element.wallet) - .eq("vesting_category", VESTING_CATEGORY) + .eq("vesting_category", VESTING_CATEGORY); - if(error) { + if (error) { console.error(error); } } - console.log("Done"); } diff --git a/test/shared/artifacts.ts b/test/shared/artifacts.ts index 06555e5d..0d519125 100644 --- a/test/shared/artifacts.ts +++ b/test/shared/artifacts.ts @@ -8,6 +8,7 @@ import USDTMock from "../../artifacts/contracts/test/ERC20Mock.sol/USDTMock.json import SmartBuilderScore from "../../artifacts/contracts/passport/SmartBuilderScore.sol/SmartBuilderScore.json"; import PassportSources from "../../artifacts/contracts/passport/PassportSources.sol/PassportSources.json"; import TalentTGEUnlock from "../../artifacts/contracts/talent/TalentTGEUnlock.sol/TalentTGEUnlock.json"; +import TalentTGEUnlockTimestamp from "../../artifacts/contracts/talent/TalentTGEUnlockTimestamp.sol/TalentTGEUnlockTimestamp.json"; export { PassportRegistry, @@ -20,4 +21,5 @@ export { SmartBuilderScore, PassportSources, TalentTGEUnlock, + TalentTGEUnlockTimestamp, }; From acccf1d0ea55166f1036fb5298291369b84f645a Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Fri, 25 Oct 2024 16:12:32 +0100 Subject: [PATCH 04/74] Add TGE unlock based on timestamp --- contracts/talent/TalentTGEUnlockTimestamp.sol | 95 +++++++++++ .../talent/TalentTGEUnlockTimestamp.ts | 160 ++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 contracts/talent/TalentTGEUnlockTimestamp.sol create mode 100644 test/contracts/talent/TalentTGEUnlockTimestamp.ts diff --git a/contracts/talent/TalentTGEUnlockTimestamp.sol b/contracts/talent/TalentTGEUnlockTimestamp.sol new file mode 100644 index 00000000..ed5eab93 --- /dev/null +++ b/contracts/talent/TalentTGEUnlockTimestamp.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +// Based on: https://github.com/gnosis/safe-token-distribution/blob/master/tooling/contracts/MerkleDistribution.sol +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../merkle/MerkleProof.sol"; + +contract TalentTGEUnlockTimestamp is Ownable { + using SafeERC20 for IERC20; + + event Claimed(address indexed claimer, uint256 amount, uint256 burned); + + address public immutable token; + bytes32 public merkleRoot; + bool public isContractEnabled; + uint256 public unlockTimestamp; + mapping(address => uint256) public claimed; + + constructor( + address _token, + bytes32 _merkleRoot, + address owner, + uint256 _unlockTimestamp + ) Ownable(owner) { + token = _token; + merkleRoot = _merkleRoot; + isContractEnabled = false; + unlockTimestamp = _unlockTimestamp; + } + + function setUnlockTimestamp(uint256 _unlockTimestamp) external onlyOwner { + unlockTimestamp = _unlockTimestamp; + } + + function disableContract() external onlyOwner { + isContractEnabled = false; + } + + function enableContract() external onlyOwner { + isContractEnabled = true; + } + + function claim( + bytes32[] calldata merkleProofClaim, + uint256 amountAllocated + ) external { + require(isContractEnabled, "Contracts are disabled"); + require(block.timestamp >= unlockTimestamp, "Unlock period not started"); + require(claimed[msg.sender] == 0, "Already claimed"); + verifyAmount(merkleProofClaim, amountAllocated); + + address beneficiary = msg.sender; + uint256 amountToClaim = calculate(beneficiary, amountAllocated); + + claimed[beneficiary] += amountToClaim; + IERC20(token).safeTransfer(beneficiary, amountToClaim); + + emit Claimed(beneficiary, amountToClaim, 0); + } + + function verifyAmount( + bytes32[] calldata proof, + uint256 amountAllocated + ) internal view { + bytes32 root = merkleRoot; + bytes32 leaf = keccak256( + bytes.concat(keccak256(abi.encode(msg.sender, amountAllocated))) + ); + + require( + MerkleProof.verify(proof, root, leaf), + "Invalid Allocation Proof" + ); + } + + function calculate( + address beneficiary, + uint256 amountAllocated + ) internal view returns (uint256 amountToClaim) { + uint256 amountClaimed = claimed[beneficiary]; + assert(amountClaimed <= amountAllocated); + amountToClaim = amountAllocated - amountClaimed; + } + + function setMerkleRoot(bytes32 nextMerkleRoot) external onlyOwner { + merkleRoot = nextMerkleRoot; + } + + function withdraw() external onlyOwner { + IERC20(token).transfer(owner(), IERC20(token).balanceOf(address(this))); + } +} diff --git a/test/contracts/talent/TalentTGEUnlockTimestamp.ts b/test/contracts/talent/TalentTGEUnlockTimestamp.ts new file mode 100644 index 00000000..cceaaad7 --- /dev/null +++ b/test/contracts/talent/TalentTGEUnlockTimestamp.ts @@ -0,0 +1,160 @@ +import chai from "chai"; +import { ethers, waffle } from "hardhat"; +import { solidity } from "ethereum-waffle"; + +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { TalentProtocolToken, TalentTGEUnlockTimestamp } from "../../../typechain-types"; +import { Artifacts } from "../../shared"; +import generateMerkleTree from "../../../functions/generateMerkleTree"; +import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; +import { BigNumber } from "ethers"; + +chai.use(solidity); + +const { expect } = chai; +const { deployContract } = waffle; + +describe("TalentTGEUnlockTimestamp", () => { + let admin: SignerWithAddress; + let user1: SignerWithAddress; + let user2: SignerWithAddress; + let user3: SignerWithAddress; + + let talentToken: TalentProtocolToken; + let TalentTGEUnlockTimestamp: TalentTGEUnlockTimestamp; + let merkleTree: StandardMerkleTree<(string | BigNumber)[]>; + let totalTalentAmount: BigNumber; + const unlockTimestamp = Math.floor(Date.now() / 1000) - 1000000; + + beforeEach(async () => { + [admin, user1, user2, user3] = await ethers.getSigners(); + + talentToken = (await deployContract(admin, Artifacts.TalentProtocolToken, [admin.address])) as TalentProtocolToken; + merkleTree = generateMerkleTree({ + [user1.address]: ethers.utils.parseUnits("10000", 18), + [user2.address]: ethers.utils.parseUnits("20000", 18), + }); + + TalentTGEUnlockTimestamp = (await deployContract(admin, Artifacts.TalentTGEUnlockTimestamp, [ + talentToken.address, + merkleTree.root, + admin.address, + unlockTimestamp, + ])) as TalentTGEUnlockTimestamp; + + // Approve TalentRewardClaim contract to spend tokens on behalf of the admin + totalTalentAmount = ethers.utils.parseUnits("600000000", 18); + await talentToken.connect(admin).transfer(TalentTGEUnlockTimestamp.address, totalTalentAmount); + await talentToken.unpause(); + }); + + describe("Deployment", () => { + it("Should set the right owner", async () => { + expect(await TalentTGEUnlockTimestamp.owner()).to.equal(admin.address); + }); + }); + + describe("Setup", () => { + it("Should not allow claims before contract is enabled", async () => { + const amount = ethers.utils.parseUnits("10000", 18); + const proof = merkleTree.getProof([user1.address, amount]); + + await expect(TalentTGEUnlockTimestamp.connect(user1).claim(proof, amount)).to.be.revertedWith( + "Contracts are disable" + ); + }); + + it("Should allow claims after contract is enabled", async () => { + const amount = ethers.utils.parseUnits("10000", 18); + const proof = merkleTree.getProof([user1.address, amount]); + + const talentAmount = await talentToken.balanceOf(user1.address); + expect(talentAmount).to.equal(0); + await TalentTGEUnlockTimestamp.connect(admin).enableContract(); + await TalentTGEUnlockTimestamp.connect(user1).claim(proof, amount); + expect(await talentToken.balanceOf(user1.address)).to.equal(amount); + }); + }); + + describe("Claiming Tokens", () => { + beforeEach(async () => { + const amounts = [ethers.utils.parseUnits("10000", 18), ethers.utils.parseUnits("20000", 18)]; + + merkleTree = generateMerkleTree({ + [user1.address]: amounts[0], + [user2.address]: amounts[1], + }); + + await TalentTGEUnlockTimestamp.setMerkleRoot(merkleTree.root); + await TalentTGEUnlockTimestamp.connect(admin).enableContract(); + }); + + it("Should allow users to claim tokens", async () => { + const proof1 = merkleTree.getProof([user1.address, ethers.utils.parseUnits("10000", 18)]); + + await TalentTGEUnlockTimestamp.connect(user1).claim(proof1, ethers.utils.parseUnits("10000", 18)); + expect(await talentToken.balanceOf(user1.address)).to.equal(ethers.utils.parseUnits("10000", 18)); + }); + + it("Should not allow claiming more than the amount", async () => { + const proof1 = merkleTree.getProof([user1.address, ethers.utils.parseUnits("10000", 18)]); + + await expect( + TalentTGEUnlockTimestamp.connect(user1).claim(proof1, ethers.utils.parseUnits("100000", 18)) + ).to.be.revertedWith("Invalid Allocation Proof"); + }); + + it("Should not allow the wrong user to claim", async () => { + const proof1 = merkleTree.getProof([user1.address, ethers.utils.parseUnits("10000", 18)]); + + await expect( + TalentTGEUnlockTimestamp.connect(user2).claim(proof1, ethers.utils.parseUnits("10000", 18)) + ).to.be.revertedWith("Invalid Allocation Proof"); + }); + }); + + describe("disable and withdraw from contract", () => { + it("Should not allow claims after contract is disabled", async () => { + const amount = ethers.utils.parseUnits("10000", 18); + const proof = merkleTree.getProof([user1.address, amount]); + + await TalentTGEUnlockTimestamp.connect(admin).disableContract(); + await expect(TalentTGEUnlockTimestamp.connect(user1).claim(proof, amount)).to.be.revertedWith( + "Contracts are disable" + ); + }); + + it("Should allow owner to set unlock timestamp", async () => { + await TalentTGEUnlockTimestamp.connect(admin).setUnlockTimestamp(unlockTimestamp); + expect(await TalentTGEUnlockTimestamp.unlockTimestamp()).to.equal(unlockTimestamp); + }); + + it("Should not allow non-owner to set unlock timestamp", async () => { + await expect(TalentTGEUnlockTimestamp.connect(user1).setUnlockTimestamp(unlockTimestamp)).to.be.revertedWith( + `OwnableUnauthorizedAccount("${user1.address}")` + ); + }); + + it("Should not allow claiming before unlock timestamp", async () => { + const amount = ethers.utils.parseUnits("10000", 18); + const proof = merkleTree.getProof([user1.address, amount]); + await TalentTGEUnlockTimestamp.connect(admin).setUnlockTimestamp(unlockTimestamp + 100000000); + await TalentTGEUnlockTimestamp.connect(admin).enableContract(); + await expect(TalentTGEUnlockTimestamp.connect(user1).claim(proof, amount)).to.be.revertedWith( + "Unlock period not started" + ); + }); + + it("Should allow owner to withdraw funds", async () => { + await TalentTGEUnlockTimestamp.connect(admin).disableContract(); + await TalentTGEUnlockTimestamp.connect(admin).withdraw(); + expect(await talentToken.balanceOf(admin.address)).to.equal(totalTalentAmount); + }); + + it("Should not allow non-owner to withdraw funds", async () => { + await expect(TalentTGEUnlockTimestamp.connect(user1).withdraw()).to.be.revertedWith( + `OwnableUnauthorizedAccount("${user1.address}")` + ); + }); + }); +}); From 2f5f935a6c38d736205745464aa8aab858830fdd Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Fri, 25 Oct 2024 17:39:20 +0100 Subject: [PATCH 05/74] Add Talent Vault base contract --- contracts/talent/TalentVault.sol | 285 +++++++++++++++++++++++++++ test/contracts/talent/TalentVault.ts | 126 ++++++++++++ test/shared/artifacts.ts | 2 + 3 files changed, 413 insertions(+) create mode 100644 contracts/talent/TalentVault.sol create mode 100644 test/contracts/talent/TalentVault.ts diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol new file mode 100644 index 00000000..e98cc324 --- /dev/null +++ b/contracts/talent/TalentVault.sol @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// Based on WLDVault.sol from Worldcoin https://optimistic.etherscan.io/address/0x21c4928109acb0659a88ae5329b5374a3024694c#code +/// @title Talent Vault Contract +/// @author Francisco Leal +/// @notice Allows any $TALENT holders to deposit their tokens and earn interest. +contract TalentVault is Ownable { + using SafeERC20 for IERC20; + + /// @notice Emitted when a user deposits tokens + /// @param user The address of the user who deposited tokens + /// @param amount The amount of tokens deposited + event Deposited(address indexed user, uint256 amount); + + /// @notice Emitted when a user withdraws tokens + /// @param user The address of the user who withdrew tokens + /// @param amount The amount of tokens withdrawn + event Withdrawn(address indexed user, uint256 amount); + + /// @notice Emitted when the yield rate is updated + /// @param yieldRate The new yield rate + event YieldRateUpdated(uint256 yieldRate); + + /// @notice Emitted when the maximum yield amount is updated + /// @param maxYieldAmount The new maximum yield amount + event MaxYieldAmountUpdated(uint256 maxYieldAmount); + + /// @notice Emitted when the yield accrual deadline is updated + /// @param yieldAccrualDeadline The new yield accrual deadline + event YieldAccrualDeadlineUpdated(uint256 yieldAccrualDeadline); + + /// @notice Represents a user's deposit + /// @param amount The amount of tokens deposited, plus any accrued interest + /// @param depositedAmount The amount of tokens that were deposited, excluding interest + /// @param lastInterestCalculation The timestamp of the last interest calculation for this deposit + struct Deposit { + uint256 amount; + uint256 depositedAmount; + uint256 lastInterestCalculation; + } + + /////////////////////////////////////////////////////////////////////////////// + /// CONFIG STORAGE /// + ////////////////////////////////////////////////////////////////////////////// + + /// @notice The number of seconds in a year + uint256 public constant SECONDS_PER_YEAR = 31536000; + + /// @notice The maximum yield rate that can be set, represented as a percentage. + uint256 public constant ONE_HUNDRED_PERCENT = 100_00; + + /// @notice The token that will be deposited into the contract + IERC20 public immutable token; + + /// @notice The wallet paying for the yield + address public yieldSource; + + /// @notice The yield rate for the contract, represented as a percentage. + /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% + uint256 public yieldRate; + + /// @notice The maximum amount of tokens that can be used to calculate interest. + uint256 public maxYieldAmount; + + /// @notice The time at which the users of the contract will stop accruing interest + uint256 public yieldAccrualDeadline; + + /// @notice A mapping of user addresses to their deposits + mapping(address => Deposit) public getDeposit; + + /// @notice Create a new Talent Vault contract + /// @param _token The token that will be deposited into the contract + /// @param _yieldRate The yield rate for the contract, with 2 decimal places (e.g. 10_00 for 10%) + /// @param _yieldSource The wallet paying for the yield + /// @param _maxYieldAmount The maximum amount of tokens that can be used to calculate interest + constructor( + IERC20 _token, + uint256 _yieldRate, + address _yieldSource, + uint256 _maxYieldAmount + ) Ownable(msg.sender) { + require( + address(_token) != address(0) && + address(_yieldSource) != address(0), + "Invalid address" + ); + + token = _token; + yieldRate = _yieldRate; + yieldSource = _yieldSource; + maxYieldAmount = _maxYieldAmount; + } + + /// @notice Deposit tokens into a user's account, which will start accruing interest. + /// @param account The address of the user to deposit tokens for + /// @param amount The amount of tokens to deposit + function depositForAddress(address account, uint256 amount) public { + require(amount > 0, "Invalid deposit amount"); + require(token.balanceOf(msg.sender) >= amount, "Insufficient balance"); + require(token.allowance(msg.sender, address(this)) >= amount, "Insufficient allowance"); + + Deposit storage userDeposit = getDeposit[account]; + + if (userDeposit.amount > 0) { + uint256 interest = calculateInterest(userDeposit); + userDeposit.amount += interest; + } + + userDeposit.amount += amount; + userDeposit.depositedAmount += amount; + userDeposit.lastInterestCalculation = block.timestamp; + + emit Deposited(account, amount); + + require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); + } + + /// @notice Deposit tokens into the contract, which will start accruing interest. + /// @param amount The amount of tokens to deposit + function deposit(uint256 amount) public { + depositForAddress(msg.sender, amount); + } + + /// @notice Calculate any accrued interest. + /// @param account The address of the user to refresh + function refreshForAddress(address account) public { + Deposit storage userDeposit = getDeposit[account]; + require(userDeposit.amount > 0, "No deposit found"); + refreshInterest(userDeposit); + } + + /// @notice Calculate any accrued interest. + function refresh() external { + refreshForAddress(msg.sender); + } + + /// @notice Returns the balance of the user, including any accrued interest. + /// @param user The address of the user to check the balance of + function balanceOf(address user) public view returns (uint256) { + Deposit storage userDeposit = getDeposit[user]; + if (userDeposit.amount == 0) return 0; + + uint256 interest = calculateInterest(userDeposit); + + return userDeposit.amount + interest; + } + + /// @notice Withdraws the requested amount from the user's balance. + function withdraw(uint256 amount) external { + _withdraw(msg.sender, amount); + } + + /// @notice Withdraws all of the user's balance, including any accrued interest. + function withdrawAll() external { + _withdraw(msg.sender, balanceOf(msg.sender)); + } + + function recoverDeposit() external { + Deposit storage userDeposit = getDeposit[msg.sender]; + require(userDeposit.amount > 0, "No deposit found"); + + refreshInterest(userDeposit); + uint256 amount = userDeposit.depositedAmount; + + userDeposit.amount -= amount; + userDeposit.depositedAmount = 0; + + emit Withdrawn(msg.sender, amount); + require(token.balanceOf(address(this)) >= amount, "Contract insolvent"); + require(token.transfer(msg.sender, amount), "Transfer failed"); + } + + /// @notice Update the yield rate for the contract + /// @dev Can only be called by the owner + function setYieldRate(uint256 _yieldRate) external onlyOwner { + require(_yieldRate > yieldRate, "Yield rate cannot be decreased"); + + yieldRate = _yieldRate; + emit YieldRateUpdated(_yieldRate); + } + + /// @notice Update the maximum amount of tokens that can be used to calculate interest + /// @dev Can only be called by the owner + function setMaxYieldAmount(uint256 _maxYieldAmount) external onlyOwner { + maxYieldAmount = _maxYieldAmount; + + emit MaxYieldAmountUpdated(_maxYieldAmount); + } + + /// @notice Update the time at which the users of the contract will stop accruing interest + /// @dev Can only be called by the owner + function setYieldAccrualDeadline( + uint256 _yieldAccrualDeadline + ) external onlyOwner { + require(_yieldAccrualDeadline > block.timestamp, "Invalid yield accrual deadline"); + + yieldAccrualDeadline = _yieldAccrualDeadline; + + emit YieldAccrualDeadlineUpdated(_yieldAccrualDeadline); + } + + /// @notice Prevents the owner from renouncing ownership + /// @dev Can only be called by the owner + function renounceOwnership() public view override onlyOwner { + revert("Cannot renounce ownership"); + } + + /// @dev Calculates the interest accrued on the deposit + /// @param userDeposit The user's deposit + /// @return The amount of interest accrued + function calculateInterest( + Deposit memory userDeposit + ) internal view returns (uint256) { + if (userDeposit.amount > maxYieldAmount) { + userDeposit.amount = maxYieldAmount; + } + + uint256 endTime; + if (yieldAccrualDeadline != 0 && block.timestamp > yieldAccrualDeadline) { + endTime = yieldAccrualDeadline; + } else { + endTime = block.timestamp; + } + + uint256 timeElapsed; + if (block.timestamp > endTime) { + timeElapsed = endTime > userDeposit.lastInterestCalculation + ? endTime - userDeposit.lastInterestCalculation + : 0; + } else { + timeElapsed = block.timestamp - userDeposit.lastInterestCalculation; + } + + return + (userDeposit.amount * yieldRate * timeElapsed) / + (SECONDS_PER_YEAR * ONE_HUNDRED_PERCENT); + } + + /// @dev Refreshes the interest on a user's deposit + /// @param userDeposit The user's deposit + function refreshInterest(Deposit storage userDeposit) internal { + if (userDeposit.amount == 0) return; + + uint256 interest = calculateInterest(userDeposit); + userDeposit.amount += interest; + userDeposit.lastInterestCalculation = block.timestamp; + } + + /// @dev Withdraws the user's balance, including any accrued interest + /// @param user The address of the user to withdraw the balance of + /// @param amount The amount of tokens to withdraw + function _withdraw(address user, uint256 amount) internal { + Deposit storage userDeposit = getDeposit[user]; + require(userDeposit.amount > 0, "No deposit found"); + + refreshInterest(userDeposit); + require(userDeposit.amount >= amount, "Not enough balance"); + + uint256 contractBalance = token.balanceOf(address(this)); + uint256 fromContractAmount = amount < userDeposit.depositedAmount + ? amount + : userDeposit.depositedAmount; + uint256 fromYieldSourceAmount = amount - fromContractAmount; + + require(contractBalance >= fromContractAmount, "Contract insolvent"); + + userDeposit.amount -= amount; + userDeposit.depositedAmount -= fromContractAmount; + + emit Withdrawn(user, amount); + + if (fromContractAmount > 0) { + require(token.transfer(user, fromContractAmount), "Transfer failed"); + } + + if (fromYieldSourceAmount > 0) { + require(token.transferFrom(yieldSource, user, fromYieldSourceAmount), "Transfer failed"); + } + } +} \ No newline at end of file diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts new file mode 100644 index 00000000..65775441 --- /dev/null +++ b/test/contracts/talent/TalentVault.ts @@ -0,0 +1,126 @@ +import chai from "chai"; +import { ethers, waffle } from "hardhat"; +import { solidity } from "ethereum-waffle"; + +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { TalentProtocolToken, TalentVault } from "../../../typechain-types"; +import { Artifacts } from "../../shared"; + +chai.use(solidity); + +const { expect } = chai; +const { deployContract } = waffle; + +describe("TalentVault", () => { + let admin: SignerWithAddress; + let user1: SignerWithAddress; + let user2: SignerWithAddress; + let user3: SignerWithAddress; + + let talentToken: TalentProtocolToken; + let talentVault: TalentVault; + + beforeEach(async () => { + [admin, user1, user2, user3] = await ethers.getSigners(); + + talentToken = (await deployContract(admin, Artifacts.TalentProtocolToken, [admin.address])) as TalentProtocolToken; + talentVault = (await deployContract(admin, Artifacts.TalentVault, [ + talentToken.address, + 10_00, + admin.address, + ethers.utils.parseEther("500000"), + ])) as TalentVault; + + // Approve TalentVault contract to spend tokens on behalf of the admin + const totalAllowance = ethers.utils.parseUnits("600000000", 18); + await talentToken.approve(talentVault.address, totalAllowance); + await talentToken.unpause(); + await talentToken.renounceOwnership(); + }); + + describe("Deployment", () => { + it("Should set the right owner", async () => { + expect(await talentVault.owner()).to.equal(admin.address); + }); + + it("Should set the correct initial values", async () => { + expect(await talentVault.yieldRate()).to.equal(10_00); + expect(await talentVault.maxYieldAmount()).to.equal(ethers.utils.parseEther("500000")); + }); + }); + + describe("Deposits", () => { + it("Should allow users to deposit tokens", async () => { + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + + await expect(talentVault.connect(user1).deposit(depositAmount)) + .to.emit(talentVault, "Deposited") + .withArgs(user1.address, depositAmount); + + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.equal(depositAmount); + }); + + it("Should not allow deposits of zero tokens", async () => { + await expect(talentVault.connect(user1).deposit(0)).to.be.revertedWith("Invalid deposit amount"); + }); + }); + + describe("Withdrawals", () => { + beforeEach(async () => { + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount); + }); + + it("Should allow users to withdraw tokens", async () => { + const withdrawAmount = ethers.utils.parseEther("500"); + await expect(talentVault.connect(user1).withdraw(withdrawAmount)) + .to.emit(talentVault, "Withdrawn") + .withArgs(user1.address, withdrawAmount); + + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(ethers.utils.parseEther("500"), ethers.utils.parseEther("0.1")); + }); + + it("Should not allow withdrawals of more than the balance", async () => { + const withdrawAmount = ethers.utils.parseEther("1500"); + await expect(talentVault.connect(user1).withdraw(withdrawAmount)).to.be.revertedWith("Not enough balance"); + }); + }); + + describe("Interest Calculation", () => { + it("Should calculate interest correctly", async () => { + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount); + + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); + + const expectedInterest = depositAmount.mul(10).div(100); // 10% interest + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.equal(depositAmount.add(expectedInterest)); + }); + }); + + describe("Administrative Functions", () => { + it("Should allow the owner to update the yield rate", async () => { + const newYieldRate = 15_00; // 15% + await talentVault.connect(admin).setYieldRate(newYieldRate); + expect(await talentVault.yieldRate()).to.equal(newYieldRate); + }); + + it("Should not allow non-owners to update the yield rate", async () => { + const newYieldRate = 15_00; // 15% + await expect(talentVault.connect(user1).setYieldRate(newYieldRate)).to.be.revertedWith( + `OwnableUnauthorizedAccount("${user1.address}")` + ); + }); + }); +}); diff --git a/test/shared/artifacts.ts b/test/shared/artifacts.ts index 0d519125..ef4319b6 100644 --- a/test/shared/artifacts.ts +++ b/test/shared/artifacts.ts @@ -9,6 +9,7 @@ import SmartBuilderScore from "../../artifacts/contracts/passport/SmartBuilderSc import PassportSources from "../../artifacts/contracts/passport/PassportSources.sol/PassportSources.json"; import TalentTGEUnlock from "../../artifacts/contracts/talent/TalentTGEUnlock.sol/TalentTGEUnlock.json"; import TalentTGEUnlockTimestamp from "../../artifacts/contracts/talent/TalentTGEUnlockTimestamp.sol/TalentTGEUnlockTimestamp.json"; +import TalentVault from "../../artifacts/contracts/talent/TalentVault.sol/TalentVault.json"; export { PassportRegistry, @@ -22,4 +23,5 @@ export { PassportSources, TalentTGEUnlock, TalentTGEUnlockTimestamp, + TalentVault, }; From 8e4502a2ee01b43e82f992c9e57f7ba2d105c2dd Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Fri, 25 Oct 2024 18:01:12 +0100 Subject: [PATCH 06/74] apply lint --- contracts/talent/TalentVault.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index e98cc324..d35cdae0 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -5,7 +5,8 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -/// Based on WLDVault.sol from Worldcoin https://optimistic.etherscan.io/address/0x21c4928109acb0659a88ae5329b5374a3024694c#code +/// Based on WLDVault.sol from Worldcoin +/// ref: https://optimistic.etherscan.io/address/0x21c4928109acb0659a88ae5329b5374a3024694c#code /// @title Talent Vault Contract /// @author Francisco Leal /// @notice Allows any $TALENT holders to deposit their tokens and earn interest. From af420a6d142973e6a0c75ab06129b7c62978cb30 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Sat, 26 Oct 2024 19:29:27 +0100 Subject: [PATCH 07/74] Add different tiers --- contracts/talent/TalentVault.sol | 67 +++++++++++++++++----- test/contracts/talent/TalentVault.ts | 84 ++++++++++++++++++++++++++-- 2 files changed, 132 insertions(+), 19 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index d35cdae0..0a1e6375 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -4,13 +4,15 @@ pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "../passport/PassportBuilderScore.sol"; /// Based on WLDVault.sol from Worldcoin /// ref: https://optimistic.etherscan.io/address/0x21c4928109acb0659a88ae5329b5374a3024694c#code /// @title Talent Vault Contract /// @author Francisco Leal /// @notice Allows any $TALENT holders to deposit their tokens and earn interest. -contract TalentVault is Ownable { +contract TalentVault is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; /// @notice Emitted when a user deposits tokens @@ -43,12 +45,9 @@ contract TalentVault is Ownable { uint256 amount; uint256 depositedAmount; uint256 lastInterestCalculation; + address user; } - /////////////////////////////////////////////////////////////////////////////// - /// CONFIG STORAGE /// - ////////////////////////////////////////////////////////////////////////////// - /// @notice The number of seconds in a year uint256 public constant SECONDS_PER_YEAR = 31536000; @@ -61,9 +60,21 @@ contract TalentVault is Ownable { /// @notice The wallet paying for the yield address public yieldSource; - /// @notice The yield rate for the contract, represented as a percentage. + /// @notice The yield base rate for the contract, represented as a percentage. + /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% + uint256 public yieldRateBase; + + /// @notice The yield rate for the contract for competent builders, represented as a percentage. + /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% + uint256 public yieldRateCompetent; + + /// @notice The yield rate for the contract for proficient builders, represented as a percentage. + /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% + uint256 public yieldRateProficient; + + /// @notice The yield rate for the contract for expert builders, represented as a percentage. /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% - uint256 public yieldRate; + uint256 public yieldRateExpert; /// @notice The maximum amount of tokens that can be used to calculate interest. uint256 public maxYieldAmount; @@ -71,19 +82,21 @@ contract TalentVault is Ownable { /// @notice The time at which the users of the contract will stop accruing interest uint256 public yieldAccrualDeadline; + PassportBuilderScore public passportBuilderScore; + /// @notice A mapping of user addresses to their deposits mapping(address => Deposit) public getDeposit; /// @notice Create a new Talent Vault contract /// @param _token The token that will be deposited into the contract - /// @param _yieldRate The yield rate for the contract, with 2 decimal places (e.g. 10_00 for 10%) /// @param _yieldSource The wallet paying for the yield /// @param _maxYieldAmount The maximum amount of tokens that can be used to calculate interest + /// @param _passportBuilderScore The Passport Builder Score contract constructor( IERC20 _token, - uint256 _yieldRate, address _yieldSource, - uint256 _maxYieldAmount + uint256 _maxYieldAmount, + PassportBuilderScore _passportBuilderScore ) Ownable(msg.sender) { require( address(_token) != address(0) && @@ -92,9 +105,13 @@ contract TalentVault is Ownable { ); token = _token; - yieldRate = _yieldRate; + yieldRateBase = 10_00; + yieldRateProficient = 15_00; + yieldRateCompetent = 20_00; + yieldRateExpert = 25_00; yieldSource = _yieldSource; maxYieldAmount = _maxYieldAmount; + passportBuilderScore = _passportBuilderScore; } /// @notice Deposit tokens into a user's account, which will start accruing interest. @@ -115,6 +132,7 @@ contract TalentVault is Ownable { userDeposit.amount += amount; userDeposit.depositedAmount += amount; userDeposit.lastInterestCalculation = block.timestamp; + userDeposit.user = account; emit Deposited(account, amount); @@ -152,12 +170,12 @@ contract TalentVault is Ownable { } /// @notice Withdraws the requested amount from the user's balance. - function withdraw(uint256 amount) external { + function withdraw(uint256 amount) external nonReentrant { _withdraw(msg.sender, amount); } /// @notice Withdraws all of the user's balance, including any accrued interest. - function withdrawAll() external { + function withdrawAll() external nonReentrant { _withdraw(msg.sender, balanceOf(msg.sender)); } @@ -179,12 +197,24 @@ contract TalentVault is Ownable { /// @notice Update the yield rate for the contract /// @dev Can only be called by the owner function setYieldRate(uint256 _yieldRate) external onlyOwner { - require(_yieldRate > yieldRate, "Yield rate cannot be decreased"); + require(_yieldRate > yieldRateBase, "Yield rate cannot be decreased"); - yieldRate = _yieldRate; + yieldRateBase = _yieldRate; emit YieldRateUpdated(_yieldRate); } + /// @notice Get the yield rate for the contract for a given user + /// @param user The address of the user to get the yield rate for + function getYieldRateForScore(address user) public view returns (uint256) { + uint256 passportId = passportBuilderScore.passportRegistry().passportId(user); + uint256 builderScore = passportBuilderScore.getScore(passportId); + + if (builderScore < 25) return yieldRateBase; + if (builderScore < 50) return yieldRateProficient; + if (builderScore < 75) return yieldRateCompetent; + return yieldRateExpert; + } + /// @notice Update the maximum amount of tokens that can be used to calculate interest /// @dev Can only be called by the owner function setMaxYieldAmount(uint256 _maxYieldAmount) external onlyOwner { @@ -211,6 +241,12 @@ contract TalentVault is Ownable { revert("Cannot renounce ownership"); } + /// @notice Set the Passport Builder Score contract + /// @dev Can only be called by the owner + function setPassportBuilderScore(PassportBuilderScore _passportBuilderScore) external onlyOwner { + passportBuilderScore = _passportBuilderScore; + } + /// @dev Calculates the interest accrued on the deposit /// @param userDeposit The user's deposit /// @return The amount of interest accrued @@ -237,6 +273,7 @@ contract TalentVault is Ownable { timeElapsed = block.timestamp - userDeposit.lastInterestCalculation; } + uint256 yieldRate = getYieldRateForScore(userDeposit.user); return (userDeposit.amount * yieldRate * timeElapsed) / (SECONDS_PER_YEAR * ONE_HUNDRED_PERCENT); diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 65775441..407b783f 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -3,7 +3,7 @@ import { ethers, waffle } from "hardhat"; import { solidity } from "ethereum-waffle"; import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { TalentProtocolToken, TalentVault } from "../../../typechain-types"; +import { TalentProtocolToken, TalentVault, PassportRegistry, PassportBuilderScore } from "../../../typechain-types"; import { Artifacts } from "../../shared"; chai.use(solidity); @@ -18,17 +18,24 @@ describe("TalentVault", () => { let user3: SignerWithAddress; let talentToken: TalentProtocolToken; + let passportRegistry: PassportRegistry; + let passportBuilderScore: PassportBuilderScore; let talentVault: TalentVault; beforeEach(async () => { [admin, user1, user2, user3] = await ethers.getSigners(); talentToken = (await deployContract(admin, Artifacts.TalentProtocolToken, [admin.address])) as TalentProtocolToken; + passportRegistry = (await deployContract(admin, Artifacts.PassportRegistry, [admin.address])) as PassportRegistry; + passportBuilderScore = (await deployContract(admin, Artifacts.PassportBuilderScore, [ + passportRegistry.address, + admin.address, + ])) as PassportBuilderScore; talentVault = (await deployContract(admin, Artifacts.TalentVault, [ talentToken.address, - 10_00, admin.address, ethers.utils.parseEther("500000"), + passportBuilderScore.address, ])) as TalentVault; // Approve TalentVault contract to spend tokens on behalf of the admin @@ -44,7 +51,10 @@ describe("TalentVault", () => { }); it("Should set the correct initial values", async () => { - expect(await talentVault.yieldRate()).to.equal(10_00); + expect(await talentVault.yieldRateBase()).to.equal(10_00); + expect(await talentVault.yieldRateProficient()).to.equal(15_00); + expect(await talentVault.yieldRateCompetent()).to.equal(20_00); + expect(await talentVault.yieldRateExpert()).to.equal(25_00); expect(await talentVault.maxYieldAmount()).to.equal(ethers.utils.parseEther("500000")); }); }); @@ -107,13 +117,79 @@ describe("TalentVault", () => { const userBalance = await talentVault.balanceOf(user1.address); expect(userBalance).to.equal(depositAmount.add(expectedInterest)); }); + + it("Should calculate interest correctly for builders with scores below 50", async () => { + await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode + await passportRegistry.connect(user1).create("source1"); + + const passportId = await passportRegistry.passportId(user1.address); + await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount); + + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); + + await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 + + const expectedInterest = depositAmount.mul(15).div(100); // 15% interest + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + }); + + it("Should calculate interest correctly for builders with scores above 50", async () => { + await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode + await passportRegistry.connect(user1).create("source1"); + + const passportId = await passportRegistry.passportId(user1.address); + await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount); + + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); + + await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 + + const expectedInterest = depositAmount.mul(20).div(100); // 20% interest + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + }); + + it("Should calculate interest correctly for builders with scores above 75", async () => { + await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode + await passportRegistry.connect(user1).create("source1"); + + const passportId = await passportRegistry.passportId(user1.address); + await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount); + + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); + + await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 + + const expectedInterest = depositAmount.mul(25).div(100); // 25% interest + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + }); }); describe("Administrative Functions", () => { it("Should allow the owner to update the yield rate", async () => { const newYieldRate = 15_00; // 15% await talentVault.connect(admin).setYieldRate(newYieldRate); - expect(await talentVault.yieldRate()).to.equal(newYieldRate); + expect(await talentVault.yieldRateBase()).to.equal(newYieldRate); }); it("Should not allow non-owners to update the yield rate", async () => { From c3a629b48513a41e3e8de6b98392f232a6d0a96f Mon Sep 17 00:00:00 2001 From: Ruben Dinis Date: Tue, 15 Oct 2024 17:53:12 +0100 Subject: [PATCH 08/74] Setup deploy scripts --- scripts/passport/deployScorer.ts | 9 +- scripts/talent/deployBuilderdropUnlocks.ts | 102 +++++++++++++++++++++ scripts/talent/deployPurchasesUnlocks.ts | 102 +++++++++++++++++++++ scripts/talent/deployTalentRewardClaim.ts | 54 +++-------- 4 files changed, 222 insertions(+), 45 deletions(-) create mode 100644 scripts/talent/deployBuilderdropUnlocks.ts create mode 100644 scripts/talent/deployPurchasesUnlocks.ts diff --git a/scripts/passport/deployScorer.ts b/scripts/passport/deployScorer.ts index c56976a2..7dcd9886 100644 --- a/scripts/passport/deployScorer.ts +++ b/scripts/passport/deployScorer.ts @@ -4,6 +4,9 @@ import { deployPassportBuilderScore, deploySmartBuilderScore } from "../shared"; const PASSPORT_MAINNET = "0xb477A9BD2547ad61f4Ac22113172Dd909E5B2331"; const PASSPORT_TESTNET = "0xa600b3356c1440B6D6e57b0B7862dC3dFB66bc43"; +const FEE_RECEIVER_MAINNET = "0xC925bD0E839E8e22A7DDEbe7f4C21b187deeC358"; +const FEE_RECEIVER_TESTNET = "0x08BC8a92e5C99755C675A21BC4FcfFb59E0A9508"; + async function main() { console.log(`Deploying passport builder score at ${network.name}`); @@ -11,15 +14,15 @@ async function main() { console.log(`Admin will be ${admin.address}`); - const builderScore = await deployPassportBuilderScore(PASSPORT_MAINNET, admin.address); + const builderScore = await deployPassportBuilderScore(PASSPORT_TESTNET, admin.address); console.log(`Scorer Address: ${builderScore.address}`); console.log(`Scorer owner: ${await builderScore.owner()}`); const smartBuilderScore = await deploySmartBuilderScore( admin.address, - PASSPORT_MAINNET, - "0xC925bD0E839E8e22A7DDEbe7f4C21b187deeC358", + PASSPORT_TESTNET, + FEE_RECEIVER_TESTNET, builderScore.address ); diff --git a/scripts/talent/deployBuilderdropUnlocks.ts b/scripts/talent/deployBuilderdropUnlocks.ts new file mode 100644 index 00000000..7428efd1 --- /dev/null +++ b/scripts/talent/deployBuilderdropUnlocks.ts @@ -0,0 +1,102 @@ +import { ethers, network } from "hardhat"; +import { deployTalentTGEUnlock } from "../shared"; +import fs from "fs"; + +import { BigNumberish } from "ethers"; + +import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; + +import distributionSetup from "../data/summerBuilderdrop.json"; +import { createClient } from "@supabase/supabase-js"; + +const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; +const TALENT_TOKEN_ADDRESS_MAINNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; + +const VESTING_CATEGORY = "summer_builderdrop" + +type BalanceMap = { + [key: string]: BigNumberish; +}; + +function generateMerkleTree(snapshot: BalanceMap): StandardMerkleTree<(string | BigNumberish)[]> { + const leaves = Object.keys(snapshot).map((address) => [address, snapshot[address]]); + + return StandardMerkleTree.of(leaves, ["address", "uint256"]); +} + +async function main() { + console.log(`Deploying TGE Unlocks at ${network.name}`); + + const [admin] = await ethers.getSigners(); + + console.log(`Admin will be ${admin.address}`); + + if(!process.env.PUBLIC_SUPABASE_URL) { + console.error("Missing PUBLIC_SUPABASE_URL"); + return 0; + } + + if(!process.env.PUBLIC_SUPABASE_ANON_KEY) { + console.error("Missing PUBLIC_SUPABASE_ANON_KEY"); + return 0; + } + + const allResults = distributionSetup as { amount: string; wallet: string }[]; + + const merkleBase = allResults.reduce((acc, { wallet, amount }) => { + acc[wallet.toLowerCase()] = ethers.utils.parseEther(amount).toBigInt(); + return acc; + }, {} as Record); + + const merkleTree = generateMerkleTree(merkleBase); + console.log("Generated merkle trees: ", merkleTree.root); + const tgeUnlockDistribution = await deployTalentTGEUnlock(TALENT_TOKEN_ADDRESS_TESTNET, admin.address, merkleTree.root); + + console.log(`TGE Unlock distribution deployed at ${tgeUnlockDistribution.address}`); + const proofList = allResults.map(({ wallet, amount }) => { + const value = ethers.utils.parseEther(amount); + const proof = merkleTree.getProof([wallet.toLowerCase(), value]); + return { + wallet, + value, + proof, + }; + }); + + console.log("Writing proofs to file"); + fs.writeFileSync( + `./data/${VESTING_CATEGORY}-proofs.json`, + JSON.stringify(proofList, (key, value) => (typeof value === "bigint" ? value.toString() : value)) + ); + + console.log("Uploading proofs to database"); + + const supabase = createClient(process.env.PUBLIC_SUPABASE_URL, process.env.PUBLIC_SUPABASE_ANON_KEY) + + const proofsCount = proofList.length + for (let i = 0; i < proofsCount; i++) { + const element = proofList[i] + + console.log(`Uploading ${i + 1}/${proofsCount}: ${element.wallet}`) + + const { error } = await supabase + .from("distributions") + .update({ proof: element.proof }) + .eq("wallet", element.wallet) + .eq("vesting_category", VESTING_CATEGORY) + + if(error) { + console.error(error); + } + } + + + console.log("Done"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/talent/deployPurchasesUnlocks.ts b/scripts/talent/deployPurchasesUnlocks.ts new file mode 100644 index 00000000..0ca28581 --- /dev/null +++ b/scripts/talent/deployPurchasesUnlocks.ts @@ -0,0 +1,102 @@ +import { ethers, network } from "hardhat"; +import { deployTalentTGEUnlock } from "../shared"; +import fs from "fs"; + +import { BigNumberish } from "ethers"; + +import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; + +import distributionSetup from "../data/inAppPurchases.json"; +import { createClient } from "@supabase/supabase-js"; + +const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; +const TALENT_TOKEN_ADDRESS_MAINNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; + +const VESTING_CATEGORY = "ecosystem_incentives_02" + +type BalanceMap = { + [key: string]: BigNumberish; +}; + +function generateMerkleTree(snapshot: BalanceMap): StandardMerkleTree<(string | BigNumberish)[]> { + const leaves = Object.keys(snapshot).map((address) => [address, snapshot[address]]); + + return StandardMerkleTree.of(leaves, ["address", "uint256"]); +} + +async function main() { + console.log(`Deploying TGE Unlocks at ${network.name}`); + + const [admin] = await ethers.getSigners(); + + console.log(`Admin will be ${admin.address}`); + + if(!process.env.PUBLIC_SUPABASE_URL) { + console.error("Missing PUBLIC_SUPABASE_URL"); + return 0; + } + + if(!process.env.PUBLIC_SUPABASE_ANON_KEY) { + console.error("Missing PUBLIC_SUPABASE_ANON_KEY"); + return 0; + } + + const allResults = distributionSetup as { amount: string; wallet: string }[]; + + const merkleBase = allResults.reduce((acc, { wallet, amount }) => { + acc[wallet.toLowerCase()] = ethers.utils.parseEther(amount).toBigInt(); + return acc; + }, {} as Record); + + const merkleTree = generateMerkleTree(merkleBase); + console.log("Generated merkle trees: ", merkleTree.root); + const tgeUnlockDistribution = await deployTalentTGEUnlock(TALENT_TOKEN_ADDRESS_TESTNET, admin.address, merkleTree.root); + + console.log(`TGE Unlock distribution deployed at ${tgeUnlockDistribution.address}`); + const proofList = allResults.map(({ wallet, amount }) => { + const value = ethers.utils.parseEther(amount); + const proof = merkleTree.getProof([wallet.toLowerCase(), value]); + return { + wallet, + value, + proof, + }; + }); + + console.log("Writing proofs to file"); + fs.writeFileSync( + "./data/inAppPutchasesProofs.json", + JSON.stringify(proofList, (key, value) => (typeof value === "bigint" ? value.toString() : value)) + ); + + console.log("Uploading proofs to database"); + + const supabase = createClient(process.env.PUBLIC_SUPABASE_URL, process.env.PUBLIC_SUPABASE_ANON_KEY) + + const proofsCount = proofList.length + for (let i = 0; i < proofsCount; i++) { + const element = proofList[i] + + console.log(`Uploading ${i + 1}/${proofsCount}: ${element.wallet}`) + + const { error } = await supabase + .from("distributions") + .update({ proof: element.proof }) + .eq("wallet", element.wallet) + .eq("vesting_category", VESTING_CATEGORY) + + if(error) { + console.error(error); + } + } + + + console.log("Done"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/talent/deployTalentRewardClaim.ts b/scripts/talent/deployTalentRewardClaim.ts index 0b73dfc9..ae1e0ed1 100644 --- a/scripts/talent/deployTalentRewardClaim.ts +++ b/scripts/talent/deployTalentRewardClaim.ts @@ -3,22 +3,13 @@ import { deployTalentRewardClaim } from "../shared"; import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; import fs from "fs"; import { createClient } from '@supabase/supabase-js' +import rewardDistributions from "../data/rewardsDistribution.json"; -import * as TalentRewardClaim from "../../artifacts/contracts/talent/TalentRewardClaim.sol/TalentRewardClaim.json"; +const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; +const TALENT_TOKEN_ADDRESS_MAINNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; -// @TODO: add all safes and addresses that need to receive the tokens -const wallets = [ - "0xf9342d70a2a6eb46afd7b81138dee01d73b2e419", - "0xc8b74c37bd25e6ca8cb6ddf2e01058c45d341182", - "0x33041027dd8f4dc82b6e825fb37adf8f15d44053", - "0x58a35cf59d5c630c057af008a78bc67cdc2ec094", - "0x923b6bfc8cb0d9a57716a1340f7b86e8b678ecea", - "0xf924efc8830bfa1029fa0cd7a51901a5ec03de3d", - "0xa081e1da16133bb4ebc7aab1a9b0588a48d15138", - "0xe3b35ff40263385159f5705ece0223ea81730692" - ]; - -// 2471833440000000000000000 +const BUILDER_SCORE_ADDRESS_TESTNET = "0xe6b4388B1ECE6863349Ff41D79C408D9E211E59a" +const BUILDER_SCORE_ADDRESS_MAINNET = "0xe6b4388B1ECE6863349Ff41D79C408D9E211E59a" async function main() { console.log(`Deploying Talent Reward Claim at ${network.name}`); @@ -35,32 +26,13 @@ async function main() { const supabase = createClient(process.env.PUBLIC_SUPABASE_URL, process.env.PUBLIC_SUPABASE_ANON_KEY) - // Consider limit of 1000 rows; sort - const { data, error } = await supabase - .from("distributions") - .select("wallet, amount") - .eq("vesting_category", "ecosystem_incentives_03") - .in("wallet", wallets) - - console.log(data); - - if(error) { - console.error(error); - return 0; - } - - if(!data || data.length == 0) { - console.error("No data to process"); - return 0; - } - const [admin] = await ethers.getSigners(); console.log("Calculating merkle tree"); - const leaves = data.map((leave) => [ - leave.wallet, - ethers.utils.parseEther(leave.amount.toFixed(2).toString()), + const leaves = rewardDistributions.map((distribution) => [ + distribution.wallet, + ethers.utils.parseEther(distribution.amount), ]); console.log("Leaves", leaves); @@ -102,15 +74,13 @@ async function main() { console.log("Deploying..."); console.log(`Admin will be ${admin.address}`); - const talentAddress = "" // Talent Token Address - const builderScoreAddress = "" // Builder Score Contract Address - const holdingWalletAddress = "" // Holding Wallet Address + const holdingWalletAddress = admin.address // Holding Wallet Address - console.log(`Contract init args: ${talentAddress} ${builderScoreAddress} ${holdingWalletAddress} ${admin.address} ${merkleTree.root}`) + console.log(`Contract init args: ${TALENT_TOKEN_ADDRESS_TESTNET} ${BUILDER_SCORE_ADDRESS_TESTNET} ${holdingWalletAddress} ${admin.address} ${merkleTree.root}`) const talentRewardClaim = await deployTalentRewardClaim( - talentAddress, - builderScoreAddress, + TALENT_TOKEN_ADDRESS_TESTNET, + BUILDER_SCORE_ADDRESS_TESTNET, holdingWalletAddress, admin.address, merkleTree.root From 62a34fbe4d124e820009485b58adc59c62a81f40 Mon Sep 17 00:00:00 2001 From: Ruben Dinis Date: Fri, 18 Oct 2024 12:28:01 +0100 Subject: [PATCH 09/74] Add generation scripts for TGE unlocks --- scripts/shared/index.ts | 2 +- scripts/talent/deployBuilderdropUnlocks.ts | 67 +++++++++------------- scripts/talent/deployPurchasesUnlocks.ts | 9 ++- scripts/talent/deployTalentRewardClaim.ts | 46 ++++++++------- 4 files changed, 58 insertions(+), 66 deletions(-) diff --git a/scripts/shared/index.ts b/scripts/shared/index.ts index 3dc49be1..0160ced3 100644 --- a/scripts/shared/index.ts +++ b/scripts/shared/index.ts @@ -104,7 +104,7 @@ export async function deployTalentTGEUnlock( ): Promise { const talentTGEUnlockContract = await ethers.getContractFactory("TalentTGEUnlock"); - const deployedTGEUnlock = await talentTGEUnlockContract.deploy(token, owner, merkleTreeRoot); + const deployedTGEUnlock = await talentTGEUnlockContract.deploy(token, merkleTreeRoot, owner); await deployedTGEUnlock.deployed(); return deployedTGEUnlock as TalentTGEUnlock; } diff --git a/scripts/talent/deployBuilderdropUnlocks.ts b/scripts/talent/deployBuilderdropUnlocks.ts index 7428efd1..cc71067b 100644 --- a/scripts/talent/deployBuilderdropUnlocks.ts +++ b/scripts/talent/deployBuilderdropUnlocks.ts @@ -10,7 +10,7 @@ import distributionSetup from "../data/summerBuilderdrop.json"; import { createClient } from "@supabase/supabase-js"; const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; -const TALENT_TOKEN_ADDRESS_MAINNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; +const TALENT_TOKEN_ADDRESS_MAINNET = "0x9a33406165f562E16C3abD82fd1185482E01b49a"; const VESTING_CATEGORY = "summer_builderdrop" @@ -31,65 +31,52 @@ async function main() { console.log(`Admin will be ${admin.address}`); - if(!process.env.PUBLIC_SUPABASE_URL) { - console.error("Missing PUBLIC_SUPABASE_URL"); - return 0; - } - - if(!process.env.PUBLIC_SUPABASE_ANON_KEY) { - console.error("Missing PUBLIC_SUPABASE_ANON_KEY"); - return 0; - } - const allResults = distributionSetup as { amount: string; wallet: string }[]; + console.log("Generate merkle tree"); + const merkleBase = allResults.reduce((acc, { wallet, amount }) => { acc[wallet.toLowerCase()] = ethers.utils.parseEther(amount).toBigInt(); return acc; }, {} as Record); const merkleTree = generateMerkleTree(merkleBase); - console.log("Generated merkle trees: ", merkleTree.root); - const tgeUnlockDistribution = await deployTalentTGEUnlock(TALENT_TOKEN_ADDRESS_TESTNET, admin.address, merkleTree.root); - console.log(`TGE Unlock distribution deployed at ${tgeUnlockDistribution.address}`); + console.log("Generate proof list"); + + let index = 0; + let fileIndex = 0; + fs.writeFileSync(`scripts/data/builderdrop-proofs-${fileIndex}.json`, "[") + const proofList = allResults.map(({ wallet, amount }) => { - const value = ethers.utils.parseEther(amount); + const value = ethers.utils.parseEther(amount).toBigInt(); const proof = merkleTree.getProof([wallet.toLowerCase(), value]); + + index += 1; + + if(index % 100000 == 0) { + fs.appendFileSync(`scripts/data/builderdrop-proofs-${fileIndex}.json`, "]") + fileIndex +=1 + fs.writeFileSync(`scripts/data/builderdrop-proofs-${fileIndex}.json`, "[") + } + + const message = JSON.stringify({wallet, proof}) + + fs.appendFileSync(`scripts/data/builderdrop-proofs-${fileIndex}.json`, `${message},\n`) + return { wallet, - value, proof, }; }); - console.log("Writing proofs to file"); - fs.writeFileSync( - `./data/${VESTING_CATEGORY}-proofs.json`, - JSON.stringify(proofList, (key, value) => (typeof value === "bigint" ? value.toString() : value)) - ); - - console.log("Uploading proofs to database"); - - const supabase = createClient(process.env.PUBLIC_SUPABASE_URL, process.env.PUBLIC_SUPABASE_ANON_KEY) - - const proofsCount = proofList.length - for (let i = 0; i < proofsCount; i++) { - const element = proofList[i] + fs.appendFileSync(`scripts/data/builderdrop-proofs-${fileIndex}.json`, "]") - console.log(`Uploading ${i + 1}/${proofsCount}: ${element.wallet}`) + console.log(`Contract init args: ${TALENT_TOKEN_ADDRESS_TESTNET} ${merkleTree.root} ${admin.address}`) - const { error } = await supabase - .from("distributions") - .update({ proof: element.proof }) - .eq("wallet", element.wallet) - .eq("vesting_category", VESTING_CATEGORY) - - if(error) { - console.error(error); - } - } + const tgeUnlockDistribution = await deployTalentTGEUnlock(TALENT_TOKEN_ADDRESS_TESTNET, admin.address, merkleTree.root); + console.log(`TGE Unlock distribution deployed at ${tgeUnlockDistribution.address}`); console.log("Done"); } diff --git a/scripts/talent/deployPurchasesUnlocks.ts b/scripts/talent/deployPurchasesUnlocks.ts index 0ca28581..4760ca89 100644 --- a/scripts/talent/deployPurchasesUnlocks.ts +++ b/scripts/talent/deployPurchasesUnlocks.ts @@ -10,7 +10,7 @@ import distributionSetup from "../data/inAppPurchases.json"; import { createClient } from "@supabase/supabase-js"; const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; -const TALENT_TOKEN_ADDRESS_MAINNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; +const TALENT_TOKEN_ADDRESS_MAINNET = "0x9a33406165f562E16C3abD82fd1185482E01b49a"; const VESTING_CATEGORY = "ecosystem_incentives_02" @@ -43,13 +43,16 @@ async function main() { const allResults = distributionSetup as { amount: string; wallet: string }[]; + console.log("Generate merkle tree") + const merkleBase = allResults.reduce((acc, { wallet, amount }) => { acc[wallet.toLowerCase()] = ethers.utils.parseEther(amount).toBigInt(); return acc; }, {} as Record); const merkleTree = generateMerkleTree(merkleBase); - console.log("Generated merkle trees: ", merkleTree.root); + + console.log(`Contract init args: ${TALENT_TOKEN_ADDRESS_TESTNET} ${merkleTree.root} ${admin.address}`) const tgeUnlockDistribution = await deployTalentTGEUnlock(TALENT_TOKEN_ADDRESS_TESTNET, admin.address, merkleTree.root); console.log(`TGE Unlock distribution deployed at ${tgeUnlockDistribution.address}`); @@ -65,7 +68,7 @@ async function main() { console.log("Writing proofs to file"); fs.writeFileSync( - "./data/inAppPutchasesProofs.json", + `scripts/data/${VESTING_CATEGORY}-proofs.json`, JSON.stringify(proofList, (key, value) => (typeof value === "bigint" ? value.toString() : value)) ); diff --git a/scripts/talent/deployTalentRewardClaim.ts b/scripts/talent/deployTalentRewardClaim.ts index ae1e0ed1..d5efba93 100644 --- a/scripts/talent/deployTalentRewardClaim.ts +++ b/scripts/talent/deployTalentRewardClaim.ts @@ -6,10 +6,12 @@ import { createClient } from '@supabase/supabase-js' import rewardDistributions from "../data/rewardsDistribution.json"; const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; -const TALENT_TOKEN_ADDRESS_MAINNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; +const TALENT_TOKEN_ADDRESS_MAINNET = "0x9a33406165f562E16C3abD82fd1185482E01b49a"; -const BUILDER_SCORE_ADDRESS_TESTNET = "0xe6b4388B1ECE6863349Ff41D79C408D9E211E59a" -const BUILDER_SCORE_ADDRESS_MAINNET = "0xe6b4388B1ECE6863349Ff41D79C408D9E211E59a" +const BUILDER_SCORE_ADDRESS_TESTNET = "0x5f3aA689C4DCBAe505E6F6c8548DbD9b908bA71d" +const BUILDER_SCORE_ADDRESS_MAINNET = "0xBBFeDA7c4d8d9Df752542b03CdD715F790B32D0B" + +const VESTING_CATEGORY = "ecosystem_incentives_03" async function main() { console.log(`Deploying Talent Reward Claim at ${network.name}`); @@ -41,12 +43,29 @@ async function main() { console.log("Dumping tree to file"); - fs.writeFileSync("./merkeTreeForRewardClaiming.json", JSON.stringify(merkleTree.dump(), (key, value) => + fs.writeFileSync(`scripts/data/${VESTING_CATEGORY}-proofs.json`, JSON.stringify(merkleTree.dump(), (key, value) => typeof value === 'bigint' ? value.toString() : value )); + console.log("Deploying..."); + console.log(`Admin will be ${admin.address}`); + + const holdingWalletAddress = admin.address // Holding Wallet Address + + console.log(`Contract init args: ${TALENT_TOKEN_ADDRESS_TESTNET} ${BUILDER_SCORE_ADDRESS_TESTNET} ${holdingWalletAddress} ${admin.address} ${merkleTree.root}`) + + const talentRewardClaim = await deployTalentRewardClaim( + TALENT_TOKEN_ADDRESS_TESTNET, + BUILDER_SCORE_ADDRESS_TESTNET, + holdingWalletAddress, + admin.address, + merkleTree.root + ); + + console.log(`Talent Reward Claim Address: ${talentRewardClaim.address}`); + console.log("Uploading proofs to database"); const walletProof = leaves.map((leave) => [ @@ -64,30 +83,13 @@ async function main() { .from("distributions") .update({ proof: element[1] }) .eq("wallet", element[0]) - .eq("vesting_category", "ecosystem_incentives_03") + .eq("vesting_category", VESTING_CATEGORY) if(error) { console.error(error); } } - console.log("Deploying..."); - console.log(`Admin will be ${admin.address}`); - - const holdingWalletAddress = admin.address // Holding Wallet Address - - console.log(`Contract init args: ${TALENT_TOKEN_ADDRESS_TESTNET} ${BUILDER_SCORE_ADDRESS_TESTNET} ${holdingWalletAddress} ${admin.address} ${merkleTree.root}`) - - const talentRewardClaim = await deployTalentRewardClaim( - TALENT_TOKEN_ADDRESS_TESTNET, - BUILDER_SCORE_ADDRESS_TESTNET, - holdingWalletAddress, - admin.address, - merkleTree.root - ); - - console.log(`Talent Reward Claim Address: ${talentRewardClaim.address}`); - console.log("Done"); } From de55590434584755e8810bfb4ad2c528333f03bb Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Fri, 25 Oct 2024 16:11:45 +0100 Subject: [PATCH 10/74] Apply linting to unlock scripts --- scripts/talent/deployPurchasesUnlocks.ts | 35 +++++++++++++----------- test/shared/artifacts.ts | 2 ++ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/scripts/talent/deployPurchasesUnlocks.ts b/scripts/talent/deployPurchasesUnlocks.ts index 4760ca89..4bb46ddd 100644 --- a/scripts/talent/deployPurchasesUnlocks.ts +++ b/scripts/talent/deployPurchasesUnlocks.ts @@ -6,13 +6,13 @@ import { BigNumberish } from "ethers"; import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; -import distributionSetup from "../data/inAppPurchases.json"; +import distributionSetup from "../data/ecosystem-incentives-02.json"; import { createClient } from "@supabase/supabase-js"; -const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; -const TALENT_TOKEN_ADDRESS_MAINNET = "0x9a33406165f562E16C3abD82fd1185482E01b49a"; +const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; +const TALENT_TOKEN_ADDRESS_MAINNET = "0x9a33406165f562E16C3abD82fd1185482E01b49a"; -const VESTING_CATEGORY = "ecosystem_incentives_02" +const VESTING_CATEGORY = "ecosystem_incentives_02"; type BalanceMap = { [key: string]: BigNumberish; @@ -31,19 +31,19 @@ async function main() { console.log(`Admin will be ${admin.address}`); - if(!process.env.PUBLIC_SUPABASE_URL) { + if (!process.env.PUBLIC_SUPABASE_URL) { console.error("Missing PUBLIC_SUPABASE_URL"); return 0; } - if(!process.env.PUBLIC_SUPABASE_ANON_KEY) { + if (!process.env.PUBLIC_SUPABASE_ANON_KEY) { console.error("Missing PUBLIC_SUPABASE_ANON_KEY"); return 0; } const allResults = distributionSetup as { amount: string; wallet: string }[]; - console.log("Generate merkle tree") + console.log("Generate merkle tree"); const merkleBase = allResults.reduce((acc, { wallet, amount }) => { acc[wallet.toLowerCase()] = ethers.utils.parseEther(amount).toBigInt(); @@ -52,8 +52,12 @@ async function main() { const merkleTree = generateMerkleTree(merkleBase); - console.log(`Contract init args: ${TALENT_TOKEN_ADDRESS_TESTNET} ${merkleTree.root} ${admin.address}`) - const tgeUnlockDistribution = await deployTalentTGEUnlock(TALENT_TOKEN_ADDRESS_TESTNET, admin.address, merkleTree.root); + console.log(`Contract init args: ${TALENT_TOKEN_ADDRESS_MAINNET} ${merkleTree.root} ${admin.address}`); + const tgeUnlockDistribution = await deployTalentTGEUnlock( + TALENT_TOKEN_ADDRESS_MAINNET, + admin.address, + merkleTree.root + ); console.log(`TGE Unlock distribution deployed at ${tgeUnlockDistribution.address}`); const proofList = allResults.map(({ wallet, amount }) => { @@ -74,26 +78,25 @@ async function main() { console.log("Uploading proofs to database"); - const supabase = createClient(process.env.PUBLIC_SUPABASE_URL, process.env.PUBLIC_SUPABASE_ANON_KEY) + const supabase = createClient(process.env.PUBLIC_SUPABASE_URL, process.env.PUBLIC_SUPABASE_ANON_KEY); - const proofsCount = proofList.length + const proofsCount = proofList.length; for (let i = 0; i < proofsCount; i++) { - const element = proofList[i] + const element = proofList[i]; - console.log(`Uploading ${i + 1}/${proofsCount}: ${element.wallet}`) + console.log(`Uploading ${i + 1}/${proofsCount}: ${element.wallet}`); const { error } = await supabase .from("distributions") .update({ proof: element.proof }) .eq("wallet", element.wallet) - .eq("vesting_category", VESTING_CATEGORY) + .eq("vesting_category", VESTING_CATEGORY); - if(error) { + if (error) { console.error(error); } } - console.log("Done"); } diff --git a/test/shared/artifacts.ts b/test/shared/artifacts.ts index faadab73..35f206d4 100644 --- a/test/shared/artifacts.ts +++ b/test/shared/artifacts.ts @@ -9,6 +9,7 @@ import SmartBuilderScore from "../../artifacts/contracts/passport/SmartBuilderSc import PassportSources from "../../artifacts/contracts/passport/PassportSources.sol/PassportSources.json"; import TalentTGEUnlock from "../../artifacts/contracts/talent/TalentTGEUnlock.sol/TalentTGEUnlock.json"; import PassportWalletRegistry from "../../artifacts/contracts/passport/PassportWalletRegistry.sol/PassportWalletRegistry.json"; +import TalentTGEUnlockTimestamp from "../../artifacts/contracts/talent/TalentTGEUnlockTimestamp.sol/TalentTGEUnlockTimestamp.json"; export { PassportRegistry, @@ -22,4 +23,5 @@ export { PassportSources, TalentTGEUnlock, PassportWalletRegistry, + TalentTGEUnlockTimestamp, }; From 5dd9b55dd2319675bdd8b22fcb901bd728eb5116 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Fri, 25 Oct 2024 16:12:32 +0100 Subject: [PATCH 11/74] Add TGE unlock based on timestamp --- contracts/talent/TalentTGEUnlockTimestamp.sol | 95 +++++++++++ .../talent/TalentTGEUnlockTimestamp.ts | 160 ++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 contracts/talent/TalentTGEUnlockTimestamp.sol create mode 100644 test/contracts/talent/TalentTGEUnlockTimestamp.ts diff --git a/contracts/talent/TalentTGEUnlockTimestamp.sol b/contracts/talent/TalentTGEUnlockTimestamp.sol new file mode 100644 index 00000000..ed5eab93 --- /dev/null +++ b/contracts/talent/TalentTGEUnlockTimestamp.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +// Based on: https://github.com/gnosis/safe-token-distribution/blob/master/tooling/contracts/MerkleDistribution.sol +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../merkle/MerkleProof.sol"; + +contract TalentTGEUnlockTimestamp is Ownable { + using SafeERC20 for IERC20; + + event Claimed(address indexed claimer, uint256 amount, uint256 burned); + + address public immutable token; + bytes32 public merkleRoot; + bool public isContractEnabled; + uint256 public unlockTimestamp; + mapping(address => uint256) public claimed; + + constructor( + address _token, + bytes32 _merkleRoot, + address owner, + uint256 _unlockTimestamp + ) Ownable(owner) { + token = _token; + merkleRoot = _merkleRoot; + isContractEnabled = false; + unlockTimestamp = _unlockTimestamp; + } + + function setUnlockTimestamp(uint256 _unlockTimestamp) external onlyOwner { + unlockTimestamp = _unlockTimestamp; + } + + function disableContract() external onlyOwner { + isContractEnabled = false; + } + + function enableContract() external onlyOwner { + isContractEnabled = true; + } + + function claim( + bytes32[] calldata merkleProofClaim, + uint256 amountAllocated + ) external { + require(isContractEnabled, "Contracts are disabled"); + require(block.timestamp >= unlockTimestamp, "Unlock period not started"); + require(claimed[msg.sender] == 0, "Already claimed"); + verifyAmount(merkleProofClaim, amountAllocated); + + address beneficiary = msg.sender; + uint256 amountToClaim = calculate(beneficiary, amountAllocated); + + claimed[beneficiary] += amountToClaim; + IERC20(token).safeTransfer(beneficiary, amountToClaim); + + emit Claimed(beneficiary, amountToClaim, 0); + } + + function verifyAmount( + bytes32[] calldata proof, + uint256 amountAllocated + ) internal view { + bytes32 root = merkleRoot; + bytes32 leaf = keccak256( + bytes.concat(keccak256(abi.encode(msg.sender, amountAllocated))) + ); + + require( + MerkleProof.verify(proof, root, leaf), + "Invalid Allocation Proof" + ); + } + + function calculate( + address beneficiary, + uint256 amountAllocated + ) internal view returns (uint256 amountToClaim) { + uint256 amountClaimed = claimed[beneficiary]; + assert(amountClaimed <= amountAllocated); + amountToClaim = amountAllocated - amountClaimed; + } + + function setMerkleRoot(bytes32 nextMerkleRoot) external onlyOwner { + merkleRoot = nextMerkleRoot; + } + + function withdraw() external onlyOwner { + IERC20(token).transfer(owner(), IERC20(token).balanceOf(address(this))); + } +} diff --git a/test/contracts/talent/TalentTGEUnlockTimestamp.ts b/test/contracts/talent/TalentTGEUnlockTimestamp.ts new file mode 100644 index 00000000..cceaaad7 --- /dev/null +++ b/test/contracts/talent/TalentTGEUnlockTimestamp.ts @@ -0,0 +1,160 @@ +import chai from "chai"; +import { ethers, waffle } from "hardhat"; +import { solidity } from "ethereum-waffle"; + +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { TalentProtocolToken, TalentTGEUnlockTimestamp } from "../../../typechain-types"; +import { Artifacts } from "../../shared"; +import generateMerkleTree from "../../../functions/generateMerkleTree"; +import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; +import { BigNumber } from "ethers"; + +chai.use(solidity); + +const { expect } = chai; +const { deployContract } = waffle; + +describe("TalentTGEUnlockTimestamp", () => { + let admin: SignerWithAddress; + let user1: SignerWithAddress; + let user2: SignerWithAddress; + let user3: SignerWithAddress; + + let talentToken: TalentProtocolToken; + let TalentTGEUnlockTimestamp: TalentTGEUnlockTimestamp; + let merkleTree: StandardMerkleTree<(string | BigNumber)[]>; + let totalTalentAmount: BigNumber; + const unlockTimestamp = Math.floor(Date.now() / 1000) - 1000000; + + beforeEach(async () => { + [admin, user1, user2, user3] = await ethers.getSigners(); + + talentToken = (await deployContract(admin, Artifacts.TalentProtocolToken, [admin.address])) as TalentProtocolToken; + merkleTree = generateMerkleTree({ + [user1.address]: ethers.utils.parseUnits("10000", 18), + [user2.address]: ethers.utils.parseUnits("20000", 18), + }); + + TalentTGEUnlockTimestamp = (await deployContract(admin, Artifacts.TalentTGEUnlockTimestamp, [ + talentToken.address, + merkleTree.root, + admin.address, + unlockTimestamp, + ])) as TalentTGEUnlockTimestamp; + + // Approve TalentRewardClaim contract to spend tokens on behalf of the admin + totalTalentAmount = ethers.utils.parseUnits("600000000", 18); + await talentToken.connect(admin).transfer(TalentTGEUnlockTimestamp.address, totalTalentAmount); + await talentToken.unpause(); + }); + + describe("Deployment", () => { + it("Should set the right owner", async () => { + expect(await TalentTGEUnlockTimestamp.owner()).to.equal(admin.address); + }); + }); + + describe("Setup", () => { + it("Should not allow claims before contract is enabled", async () => { + const amount = ethers.utils.parseUnits("10000", 18); + const proof = merkleTree.getProof([user1.address, amount]); + + await expect(TalentTGEUnlockTimestamp.connect(user1).claim(proof, amount)).to.be.revertedWith( + "Contracts are disable" + ); + }); + + it("Should allow claims after contract is enabled", async () => { + const amount = ethers.utils.parseUnits("10000", 18); + const proof = merkleTree.getProof([user1.address, amount]); + + const talentAmount = await talentToken.balanceOf(user1.address); + expect(talentAmount).to.equal(0); + await TalentTGEUnlockTimestamp.connect(admin).enableContract(); + await TalentTGEUnlockTimestamp.connect(user1).claim(proof, amount); + expect(await talentToken.balanceOf(user1.address)).to.equal(amount); + }); + }); + + describe("Claiming Tokens", () => { + beforeEach(async () => { + const amounts = [ethers.utils.parseUnits("10000", 18), ethers.utils.parseUnits("20000", 18)]; + + merkleTree = generateMerkleTree({ + [user1.address]: amounts[0], + [user2.address]: amounts[1], + }); + + await TalentTGEUnlockTimestamp.setMerkleRoot(merkleTree.root); + await TalentTGEUnlockTimestamp.connect(admin).enableContract(); + }); + + it("Should allow users to claim tokens", async () => { + const proof1 = merkleTree.getProof([user1.address, ethers.utils.parseUnits("10000", 18)]); + + await TalentTGEUnlockTimestamp.connect(user1).claim(proof1, ethers.utils.parseUnits("10000", 18)); + expect(await talentToken.balanceOf(user1.address)).to.equal(ethers.utils.parseUnits("10000", 18)); + }); + + it("Should not allow claiming more than the amount", async () => { + const proof1 = merkleTree.getProof([user1.address, ethers.utils.parseUnits("10000", 18)]); + + await expect( + TalentTGEUnlockTimestamp.connect(user1).claim(proof1, ethers.utils.parseUnits("100000", 18)) + ).to.be.revertedWith("Invalid Allocation Proof"); + }); + + it("Should not allow the wrong user to claim", async () => { + const proof1 = merkleTree.getProof([user1.address, ethers.utils.parseUnits("10000", 18)]); + + await expect( + TalentTGEUnlockTimestamp.connect(user2).claim(proof1, ethers.utils.parseUnits("10000", 18)) + ).to.be.revertedWith("Invalid Allocation Proof"); + }); + }); + + describe("disable and withdraw from contract", () => { + it("Should not allow claims after contract is disabled", async () => { + const amount = ethers.utils.parseUnits("10000", 18); + const proof = merkleTree.getProof([user1.address, amount]); + + await TalentTGEUnlockTimestamp.connect(admin).disableContract(); + await expect(TalentTGEUnlockTimestamp.connect(user1).claim(proof, amount)).to.be.revertedWith( + "Contracts are disable" + ); + }); + + it("Should allow owner to set unlock timestamp", async () => { + await TalentTGEUnlockTimestamp.connect(admin).setUnlockTimestamp(unlockTimestamp); + expect(await TalentTGEUnlockTimestamp.unlockTimestamp()).to.equal(unlockTimestamp); + }); + + it("Should not allow non-owner to set unlock timestamp", async () => { + await expect(TalentTGEUnlockTimestamp.connect(user1).setUnlockTimestamp(unlockTimestamp)).to.be.revertedWith( + `OwnableUnauthorizedAccount("${user1.address}")` + ); + }); + + it("Should not allow claiming before unlock timestamp", async () => { + const amount = ethers.utils.parseUnits("10000", 18); + const proof = merkleTree.getProof([user1.address, amount]); + await TalentTGEUnlockTimestamp.connect(admin).setUnlockTimestamp(unlockTimestamp + 100000000); + await TalentTGEUnlockTimestamp.connect(admin).enableContract(); + await expect(TalentTGEUnlockTimestamp.connect(user1).claim(proof, amount)).to.be.revertedWith( + "Unlock period not started" + ); + }); + + it("Should allow owner to withdraw funds", async () => { + await TalentTGEUnlockTimestamp.connect(admin).disableContract(); + await TalentTGEUnlockTimestamp.connect(admin).withdraw(); + expect(await talentToken.balanceOf(admin.address)).to.equal(totalTalentAmount); + }); + + it("Should not allow non-owner to withdraw funds", async () => { + await expect(TalentTGEUnlockTimestamp.connect(user1).withdraw()).to.be.revertedWith( + `OwnableUnauthorizedAccount("${user1.address}")` + ); + }); + }); +}); From 6bd71b999a8121d5cf7615ad9a9dc2e76104ef1d Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Fri, 25 Oct 2024 17:39:20 +0100 Subject: [PATCH 12/74] Add Talent Vault base contract --- contracts/talent/TalentVault.sol | 285 +++++++++++++++++++++++++++ test/contracts/talent/TalentVault.ts | 126 ++++++++++++ test/shared/artifacts.ts | 2 + 3 files changed, 413 insertions(+) create mode 100644 contracts/talent/TalentVault.sol create mode 100644 test/contracts/talent/TalentVault.ts diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol new file mode 100644 index 00000000..e98cc324 --- /dev/null +++ b/contracts/talent/TalentVault.sol @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// Based on WLDVault.sol from Worldcoin https://optimistic.etherscan.io/address/0x21c4928109acb0659a88ae5329b5374a3024694c#code +/// @title Talent Vault Contract +/// @author Francisco Leal +/// @notice Allows any $TALENT holders to deposit their tokens and earn interest. +contract TalentVault is Ownable { + using SafeERC20 for IERC20; + + /// @notice Emitted when a user deposits tokens + /// @param user The address of the user who deposited tokens + /// @param amount The amount of tokens deposited + event Deposited(address indexed user, uint256 amount); + + /// @notice Emitted when a user withdraws tokens + /// @param user The address of the user who withdrew tokens + /// @param amount The amount of tokens withdrawn + event Withdrawn(address indexed user, uint256 amount); + + /// @notice Emitted when the yield rate is updated + /// @param yieldRate The new yield rate + event YieldRateUpdated(uint256 yieldRate); + + /// @notice Emitted when the maximum yield amount is updated + /// @param maxYieldAmount The new maximum yield amount + event MaxYieldAmountUpdated(uint256 maxYieldAmount); + + /// @notice Emitted when the yield accrual deadline is updated + /// @param yieldAccrualDeadline The new yield accrual deadline + event YieldAccrualDeadlineUpdated(uint256 yieldAccrualDeadline); + + /// @notice Represents a user's deposit + /// @param amount The amount of tokens deposited, plus any accrued interest + /// @param depositedAmount The amount of tokens that were deposited, excluding interest + /// @param lastInterestCalculation The timestamp of the last interest calculation for this deposit + struct Deposit { + uint256 amount; + uint256 depositedAmount; + uint256 lastInterestCalculation; + } + + /////////////////////////////////////////////////////////////////////////////// + /// CONFIG STORAGE /// + ////////////////////////////////////////////////////////////////////////////// + + /// @notice The number of seconds in a year + uint256 public constant SECONDS_PER_YEAR = 31536000; + + /// @notice The maximum yield rate that can be set, represented as a percentage. + uint256 public constant ONE_HUNDRED_PERCENT = 100_00; + + /// @notice The token that will be deposited into the contract + IERC20 public immutable token; + + /// @notice The wallet paying for the yield + address public yieldSource; + + /// @notice The yield rate for the contract, represented as a percentage. + /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% + uint256 public yieldRate; + + /// @notice The maximum amount of tokens that can be used to calculate interest. + uint256 public maxYieldAmount; + + /// @notice The time at which the users of the contract will stop accruing interest + uint256 public yieldAccrualDeadline; + + /// @notice A mapping of user addresses to their deposits + mapping(address => Deposit) public getDeposit; + + /// @notice Create a new Talent Vault contract + /// @param _token The token that will be deposited into the contract + /// @param _yieldRate The yield rate for the contract, with 2 decimal places (e.g. 10_00 for 10%) + /// @param _yieldSource The wallet paying for the yield + /// @param _maxYieldAmount The maximum amount of tokens that can be used to calculate interest + constructor( + IERC20 _token, + uint256 _yieldRate, + address _yieldSource, + uint256 _maxYieldAmount + ) Ownable(msg.sender) { + require( + address(_token) != address(0) && + address(_yieldSource) != address(0), + "Invalid address" + ); + + token = _token; + yieldRate = _yieldRate; + yieldSource = _yieldSource; + maxYieldAmount = _maxYieldAmount; + } + + /// @notice Deposit tokens into a user's account, which will start accruing interest. + /// @param account The address of the user to deposit tokens for + /// @param amount The amount of tokens to deposit + function depositForAddress(address account, uint256 amount) public { + require(amount > 0, "Invalid deposit amount"); + require(token.balanceOf(msg.sender) >= amount, "Insufficient balance"); + require(token.allowance(msg.sender, address(this)) >= amount, "Insufficient allowance"); + + Deposit storage userDeposit = getDeposit[account]; + + if (userDeposit.amount > 0) { + uint256 interest = calculateInterest(userDeposit); + userDeposit.amount += interest; + } + + userDeposit.amount += amount; + userDeposit.depositedAmount += amount; + userDeposit.lastInterestCalculation = block.timestamp; + + emit Deposited(account, amount); + + require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); + } + + /// @notice Deposit tokens into the contract, which will start accruing interest. + /// @param amount The amount of tokens to deposit + function deposit(uint256 amount) public { + depositForAddress(msg.sender, amount); + } + + /// @notice Calculate any accrued interest. + /// @param account The address of the user to refresh + function refreshForAddress(address account) public { + Deposit storage userDeposit = getDeposit[account]; + require(userDeposit.amount > 0, "No deposit found"); + refreshInterest(userDeposit); + } + + /// @notice Calculate any accrued interest. + function refresh() external { + refreshForAddress(msg.sender); + } + + /// @notice Returns the balance of the user, including any accrued interest. + /// @param user The address of the user to check the balance of + function balanceOf(address user) public view returns (uint256) { + Deposit storage userDeposit = getDeposit[user]; + if (userDeposit.amount == 0) return 0; + + uint256 interest = calculateInterest(userDeposit); + + return userDeposit.amount + interest; + } + + /// @notice Withdraws the requested amount from the user's balance. + function withdraw(uint256 amount) external { + _withdraw(msg.sender, amount); + } + + /// @notice Withdraws all of the user's balance, including any accrued interest. + function withdrawAll() external { + _withdraw(msg.sender, balanceOf(msg.sender)); + } + + function recoverDeposit() external { + Deposit storage userDeposit = getDeposit[msg.sender]; + require(userDeposit.amount > 0, "No deposit found"); + + refreshInterest(userDeposit); + uint256 amount = userDeposit.depositedAmount; + + userDeposit.amount -= amount; + userDeposit.depositedAmount = 0; + + emit Withdrawn(msg.sender, amount); + require(token.balanceOf(address(this)) >= amount, "Contract insolvent"); + require(token.transfer(msg.sender, amount), "Transfer failed"); + } + + /// @notice Update the yield rate for the contract + /// @dev Can only be called by the owner + function setYieldRate(uint256 _yieldRate) external onlyOwner { + require(_yieldRate > yieldRate, "Yield rate cannot be decreased"); + + yieldRate = _yieldRate; + emit YieldRateUpdated(_yieldRate); + } + + /// @notice Update the maximum amount of tokens that can be used to calculate interest + /// @dev Can only be called by the owner + function setMaxYieldAmount(uint256 _maxYieldAmount) external onlyOwner { + maxYieldAmount = _maxYieldAmount; + + emit MaxYieldAmountUpdated(_maxYieldAmount); + } + + /// @notice Update the time at which the users of the contract will stop accruing interest + /// @dev Can only be called by the owner + function setYieldAccrualDeadline( + uint256 _yieldAccrualDeadline + ) external onlyOwner { + require(_yieldAccrualDeadline > block.timestamp, "Invalid yield accrual deadline"); + + yieldAccrualDeadline = _yieldAccrualDeadline; + + emit YieldAccrualDeadlineUpdated(_yieldAccrualDeadline); + } + + /// @notice Prevents the owner from renouncing ownership + /// @dev Can only be called by the owner + function renounceOwnership() public view override onlyOwner { + revert("Cannot renounce ownership"); + } + + /// @dev Calculates the interest accrued on the deposit + /// @param userDeposit The user's deposit + /// @return The amount of interest accrued + function calculateInterest( + Deposit memory userDeposit + ) internal view returns (uint256) { + if (userDeposit.amount > maxYieldAmount) { + userDeposit.amount = maxYieldAmount; + } + + uint256 endTime; + if (yieldAccrualDeadline != 0 && block.timestamp > yieldAccrualDeadline) { + endTime = yieldAccrualDeadline; + } else { + endTime = block.timestamp; + } + + uint256 timeElapsed; + if (block.timestamp > endTime) { + timeElapsed = endTime > userDeposit.lastInterestCalculation + ? endTime - userDeposit.lastInterestCalculation + : 0; + } else { + timeElapsed = block.timestamp - userDeposit.lastInterestCalculation; + } + + return + (userDeposit.amount * yieldRate * timeElapsed) / + (SECONDS_PER_YEAR * ONE_HUNDRED_PERCENT); + } + + /// @dev Refreshes the interest on a user's deposit + /// @param userDeposit The user's deposit + function refreshInterest(Deposit storage userDeposit) internal { + if (userDeposit.amount == 0) return; + + uint256 interest = calculateInterest(userDeposit); + userDeposit.amount += interest; + userDeposit.lastInterestCalculation = block.timestamp; + } + + /// @dev Withdraws the user's balance, including any accrued interest + /// @param user The address of the user to withdraw the balance of + /// @param amount The amount of tokens to withdraw + function _withdraw(address user, uint256 amount) internal { + Deposit storage userDeposit = getDeposit[user]; + require(userDeposit.amount > 0, "No deposit found"); + + refreshInterest(userDeposit); + require(userDeposit.amount >= amount, "Not enough balance"); + + uint256 contractBalance = token.balanceOf(address(this)); + uint256 fromContractAmount = amount < userDeposit.depositedAmount + ? amount + : userDeposit.depositedAmount; + uint256 fromYieldSourceAmount = amount - fromContractAmount; + + require(contractBalance >= fromContractAmount, "Contract insolvent"); + + userDeposit.amount -= amount; + userDeposit.depositedAmount -= fromContractAmount; + + emit Withdrawn(user, amount); + + if (fromContractAmount > 0) { + require(token.transfer(user, fromContractAmount), "Transfer failed"); + } + + if (fromYieldSourceAmount > 0) { + require(token.transferFrom(yieldSource, user, fromYieldSourceAmount), "Transfer failed"); + } + } +} \ No newline at end of file diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts new file mode 100644 index 00000000..65775441 --- /dev/null +++ b/test/contracts/talent/TalentVault.ts @@ -0,0 +1,126 @@ +import chai from "chai"; +import { ethers, waffle } from "hardhat"; +import { solidity } from "ethereum-waffle"; + +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { TalentProtocolToken, TalentVault } from "../../../typechain-types"; +import { Artifacts } from "../../shared"; + +chai.use(solidity); + +const { expect } = chai; +const { deployContract } = waffle; + +describe("TalentVault", () => { + let admin: SignerWithAddress; + let user1: SignerWithAddress; + let user2: SignerWithAddress; + let user3: SignerWithAddress; + + let talentToken: TalentProtocolToken; + let talentVault: TalentVault; + + beforeEach(async () => { + [admin, user1, user2, user3] = await ethers.getSigners(); + + talentToken = (await deployContract(admin, Artifacts.TalentProtocolToken, [admin.address])) as TalentProtocolToken; + talentVault = (await deployContract(admin, Artifacts.TalentVault, [ + talentToken.address, + 10_00, + admin.address, + ethers.utils.parseEther("500000"), + ])) as TalentVault; + + // Approve TalentVault contract to spend tokens on behalf of the admin + const totalAllowance = ethers.utils.parseUnits("600000000", 18); + await talentToken.approve(talentVault.address, totalAllowance); + await talentToken.unpause(); + await talentToken.renounceOwnership(); + }); + + describe("Deployment", () => { + it("Should set the right owner", async () => { + expect(await talentVault.owner()).to.equal(admin.address); + }); + + it("Should set the correct initial values", async () => { + expect(await talentVault.yieldRate()).to.equal(10_00); + expect(await talentVault.maxYieldAmount()).to.equal(ethers.utils.parseEther("500000")); + }); + }); + + describe("Deposits", () => { + it("Should allow users to deposit tokens", async () => { + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + + await expect(talentVault.connect(user1).deposit(depositAmount)) + .to.emit(talentVault, "Deposited") + .withArgs(user1.address, depositAmount); + + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.equal(depositAmount); + }); + + it("Should not allow deposits of zero tokens", async () => { + await expect(talentVault.connect(user1).deposit(0)).to.be.revertedWith("Invalid deposit amount"); + }); + }); + + describe("Withdrawals", () => { + beforeEach(async () => { + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount); + }); + + it("Should allow users to withdraw tokens", async () => { + const withdrawAmount = ethers.utils.parseEther("500"); + await expect(talentVault.connect(user1).withdraw(withdrawAmount)) + .to.emit(talentVault, "Withdrawn") + .withArgs(user1.address, withdrawAmount); + + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(ethers.utils.parseEther("500"), ethers.utils.parseEther("0.1")); + }); + + it("Should not allow withdrawals of more than the balance", async () => { + const withdrawAmount = ethers.utils.parseEther("1500"); + await expect(talentVault.connect(user1).withdraw(withdrawAmount)).to.be.revertedWith("Not enough balance"); + }); + }); + + describe("Interest Calculation", () => { + it("Should calculate interest correctly", async () => { + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount); + + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); + + const expectedInterest = depositAmount.mul(10).div(100); // 10% interest + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.equal(depositAmount.add(expectedInterest)); + }); + }); + + describe("Administrative Functions", () => { + it("Should allow the owner to update the yield rate", async () => { + const newYieldRate = 15_00; // 15% + await talentVault.connect(admin).setYieldRate(newYieldRate); + expect(await talentVault.yieldRate()).to.equal(newYieldRate); + }); + + it("Should not allow non-owners to update the yield rate", async () => { + const newYieldRate = 15_00; // 15% + await expect(talentVault.connect(user1).setYieldRate(newYieldRate)).to.be.revertedWith( + `OwnableUnauthorizedAccount("${user1.address}")` + ); + }); + }); +}); diff --git a/test/shared/artifacts.ts b/test/shared/artifacts.ts index 35f206d4..01fd525b 100644 --- a/test/shared/artifacts.ts +++ b/test/shared/artifacts.ts @@ -10,6 +10,7 @@ import PassportSources from "../../artifacts/contracts/passport/PassportSources. import TalentTGEUnlock from "../../artifacts/contracts/talent/TalentTGEUnlock.sol/TalentTGEUnlock.json"; import PassportWalletRegistry from "../../artifacts/contracts/passport/PassportWalletRegistry.sol/PassportWalletRegistry.json"; import TalentTGEUnlockTimestamp from "../../artifacts/contracts/talent/TalentTGEUnlockTimestamp.sol/TalentTGEUnlockTimestamp.json"; +import TalentVault from "../../artifacts/contracts/talent/TalentVault.sol/TalentVault.json"; export { PassportRegistry, @@ -24,4 +25,5 @@ export { TalentTGEUnlock, PassportWalletRegistry, TalentTGEUnlockTimestamp, + TalentVault, }; From fce1d8d4409a937cde73e7cef410d350670698fc Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Fri, 25 Oct 2024 18:01:12 +0100 Subject: [PATCH 13/74] apply lint --- contracts/talent/TalentVault.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index e98cc324..d35cdae0 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -5,7 +5,8 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -/// Based on WLDVault.sol from Worldcoin https://optimistic.etherscan.io/address/0x21c4928109acb0659a88ae5329b5374a3024694c#code +/// Based on WLDVault.sol from Worldcoin +/// ref: https://optimistic.etherscan.io/address/0x21c4928109acb0659a88ae5329b5374a3024694c#code /// @title Talent Vault Contract /// @author Francisco Leal /// @notice Allows any $TALENT holders to deposit their tokens and earn interest. From a1956636d9f4dc70d42828035d25d1f95316ee6a Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Sat, 26 Oct 2024 19:29:27 +0100 Subject: [PATCH 14/74] Add different tiers --- contracts/talent/TalentVault.sol | 67 +++++++++++++++++----- test/contracts/talent/TalentVault.ts | 84 ++++++++++++++++++++++++++-- 2 files changed, 132 insertions(+), 19 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index d35cdae0..0a1e6375 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -4,13 +4,15 @@ pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "../passport/PassportBuilderScore.sol"; /// Based on WLDVault.sol from Worldcoin /// ref: https://optimistic.etherscan.io/address/0x21c4928109acb0659a88ae5329b5374a3024694c#code /// @title Talent Vault Contract /// @author Francisco Leal /// @notice Allows any $TALENT holders to deposit their tokens and earn interest. -contract TalentVault is Ownable { +contract TalentVault is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; /// @notice Emitted when a user deposits tokens @@ -43,12 +45,9 @@ contract TalentVault is Ownable { uint256 amount; uint256 depositedAmount; uint256 lastInterestCalculation; + address user; } - /////////////////////////////////////////////////////////////////////////////// - /// CONFIG STORAGE /// - ////////////////////////////////////////////////////////////////////////////// - /// @notice The number of seconds in a year uint256 public constant SECONDS_PER_YEAR = 31536000; @@ -61,9 +60,21 @@ contract TalentVault is Ownable { /// @notice The wallet paying for the yield address public yieldSource; - /// @notice The yield rate for the contract, represented as a percentage. + /// @notice The yield base rate for the contract, represented as a percentage. + /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% + uint256 public yieldRateBase; + + /// @notice The yield rate for the contract for competent builders, represented as a percentage. + /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% + uint256 public yieldRateCompetent; + + /// @notice The yield rate for the contract for proficient builders, represented as a percentage. + /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% + uint256 public yieldRateProficient; + + /// @notice The yield rate for the contract for expert builders, represented as a percentage. /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% - uint256 public yieldRate; + uint256 public yieldRateExpert; /// @notice The maximum amount of tokens that can be used to calculate interest. uint256 public maxYieldAmount; @@ -71,19 +82,21 @@ contract TalentVault is Ownable { /// @notice The time at which the users of the contract will stop accruing interest uint256 public yieldAccrualDeadline; + PassportBuilderScore public passportBuilderScore; + /// @notice A mapping of user addresses to their deposits mapping(address => Deposit) public getDeposit; /// @notice Create a new Talent Vault contract /// @param _token The token that will be deposited into the contract - /// @param _yieldRate The yield rate for the contract, with 2 decimal places (e.g. 10_00 for 10%) /// @param _yieldSource The wallet paying for the yield /// @param _maxYieldAmount The maximum amount of tokens that can be used to calculate interest + /// @param _passportBuilderScore The Passport Builder Score contract constructor( IERC20 _token, - uint256 _yieldRate, address _yieldSource, - uint256 _maxYieldAmount + uint256 _maxYieldAmount, + PassportBuilderScore _passportBuilderScore ) Ownable(msg.sender) { require( address(_token) != address(0) && @@ -92,9 +105,13 @@ contract TalentVault is Ownable { ); token = _token; - yieldRate = _yieldRate; + yieldRateBase = 10_00; + yieldRateProficient = 15_00; + yieldRateCompetent = 20_00; + yieldRateExpert = 25_00; yieldSource = _yieldSource; maxYieldAmount = _maxYieldAmount; + passportBuilderScore = _passportBuilderScore; } /// @notice Deposit tokens into a user's account, which will start accruing interest. @@ -115,6 +132,7 @@ contract TalentVault is Ownable { userDeposit.amount += amount; userDeposit.depositedAmount += amount; userDeposit.lastInterestCalculation = block.timestamp; + userDeposit.user = account; emit Deposited(account, amount); @@ -152,12 +170,12 @@ contract TalentVault is Ownable { } /// @notice Withdraws the requested amount from the user's balance. - function withdraw(uint256 amount) external { + function withdraw(uint256 amount) external nonReentrant { _withdraw(msg.sender, amount); } /// @notice Withdraws all of the user's balance, including any accrued interest. - function withdrawAll() external { + function withdrawAll() external nonReentrant { _withdraw(msg.sender, balanceOf(msg.sender)); } @@ -179,12 +197,24 @@ contract TalentVault is Ownable { /// @notice Update the yield rate for the contract /// @dev Can only be called by the owner function setYieldRate(uint256 _yieldRate) external onlyOwner { - require(_yieldRate > yieldRate, "Yield rate cannot be decreased"); + require(_yieldRate > yieldRateBase, "Yield rate cannot be decreased"); - yieldRate = _yieldRate; + yieldRateBase = _yieldRate; emit YieldRateUpdated(_yieldRate); } + /// @notice Get the yield rate for the contract for a given user + /// @param user The address of the user to get the yield rate for + function getYieldRateForScore(address user) public view returns (uint256) { + uint256 passportId = passportBuilderScore.passportRegistry().passportId(user); + uint256 builderScore = passportBuilderScore.getScore(passportId); + + if (builderScore < 25) return yieldRateBase; + if (builderScore < 50) return yieldRateProficient; + if (builderScore < 75) return yieldRateCompetent; + return yieldRateExpert; + } + /// @notice Update the maximum amount of tokens that can be used to calculate interest /// @dev Can only be called by the owner function setMaxYieldAmount(uint256 _maxYieldAmount) external onlyOwner { @@ -211,6 +241,12 @@ contract TalentVault is Ownable { revert("Cannot renounce ownership"); } + /// @notice Set the Passport Builder Score contract + /// @dev Can only be called by the owner + function setPassportBuilderScore(PassportBuilderScore _passportBuilderScore) external onlyOwner { + passportBuilderScore = _passportBuilderScore; + } + /// @dev Calculates the interest accrued on the deposit /// @param userDeposit The user's deposit /// @return The amount of interest accrued @@ -237,6 +273,7 @@ contract TalentVault is Ownable { timeElapsed = block.timestamp - userDeposit.lastInterestCalculation; } + uint256 yieldRate = getYieldRateForScore(userDeposit.user); return (userDeposit.amount * yieldRate * timeElapsed) / (SECONDS_PER_YEAR * ONE_HUNDRED_PERCENT); diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 65775441..407b783f 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -3,7 +3,7 @@ import { ethers, waffle } from "hardhat"; import { solidity } from "ethereum-waffle"; import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { TalentProtocolToken, TalentVault } from "../../../typechain-types"; +import { TalentProtocolToken, TalentVault, PassportRegistry, PassportBuilderScore } from "../../../typechain-types"; import { Artifacts } from "../../shared"; chai.use(solidity); @@ -18,17 +18,24 @@ describe("TalentVault", () => { let user3: SignerWithAddress; let talentToken: TalentProtocolToken; + let passportRegistry: PassportRegistry; + let passportBuilderScore: PassportBuilderScore; let talentVault: TalentVault; beforeEach(async () => { [admin, user1, user2, user3] = await ethers.getSigners(); talentToken = (await deployContract(admin, Artifacts.TalentProtocolToken, [admin.address])) as TalentProtocolToken; + passportRegistry = (await deployContract(admin, Artifacts.PassportRegistry, [admin.address])) as PassportRegistry; + passportBuilderScore = (await deployContract(admin, Artifacts.PassportBuilderScore, [ + passportRegistry.address, + admin.address, + ])) as PassportBuilderScore; talentVault = (await deployContract(admin, Artifacts.TalentVault, [ talentToken.address, - 10_00, admin.address, ethers.utils.parseEther("500000"), + passportBuilderScore.address, ])) as TalentVault; // Approve TalentVault contract to spend tokens on behalf of the admin @@ -44,7 +51,10 @@ describe("TalentVault", () => { }); it("Should set the correct initial values", async () => { - expect(await talentVault.yieldRate()).to.equal(10_00); + expect(await talentVault.yieldRateBase()).to.equal(10_00); + expect(await talentVault.yieldRateProficient()).to.equal(15_00); + expect(await talentVault.yieldRateCompetent()).to.equal(20_00); + expect(await talentVault.yieldRateExpert()).to.equal(25_00); expect(await talentVault.maxYieldAmount()).to.equal(ethers.utils.parseEther("500000")); }); }); @@ -107,13 +117,79 @@ describe("TalentVault", () => { const userBalance = await talentVault.balanceOf(user1.address); expect(userBalance).to.equal(depositAmount.add(expectedInterest)); }); + + it("Should calculate interest correctly for builders with scores below 50", async () => { + await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode + await passportRegistry.connect(user1).create("source1"); + + const passportId = await passportRegistry.passportId(user1.address); + await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount); + + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); + + await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 + + const expectedInterest = depositAmount.mul(15).div(100); // 15% interest + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + }); + + it("Should calculate interest correctly for builders with scores above 50", async () => { + await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode + await passportRegistry.connect(user1).create("source1"); + + const passportId = await passportRegistry.passportId(user1.address); + await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount); + + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); + + await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 + + const expectedInterest = depositAmount.mul(20).div(100); // 20% interest + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + }); + + it("Should calculate interest correctly for builders with scores above 75", async () => { + await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode + await passportRegistry.connect(user1).create("source1"); + + const passportId = await passportRegistry.passportId(user1.address); + await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount); + + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); + + await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 + + const expectedInterest = depositAmount.mul(25).div(100); // 25% interest + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + }); }); describe("Administrative Functions", () => { it("Should allow the owner to update the yield rate", async () => { const newYieldRate = 15_00; // 15% await talentVault.connect(admin).setYieldRate(newYieldRate); - expect(await talentVault.yieldRate()).to.equal(newYieldRate); + expect(await talentVault.yieldRateBase()).to.equal(newYieldRate); }); it("Should not allow non-owners to update the yield rate", async () => { From 3c88a663fe138287b64bceaed03815808bbca66d Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Mon, 28 Oct 2024 10:09:56 +0000 Subject: [PATCH 15/74] Update for final vesting deploy --- scripts/talent/deployPurchasesUnlocks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/talent/deployPurchasesUnlocks.ts b/scripts/talent/deployPurchasesUnlocks.ts index 4bb46ddd..bdaefd67 100644 --- a/scripts/talent/deployPurchasesUnlocks.ts +++ b/scripts/talent/deployPurchasesUnlocks.ts @@ -6,13 +6,13 @@ import { BigNumberish } from "ethers"; import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; -import distributionSetup from "../data/ecosystem-incentives-02.json"; +import distributionSetup from "../data/ecosystem-incentives-04.json"; import { createClient } from "@supabase/supabase-js"; const TALENT_TOKEN_ADDRESS_TESTNET = "0xb669707B3784B1284f6B6a398f6b04b1AD78C74E"; const TALENT_TOKEN_ADDRESS_MAINNET = "0x9a33406165f562E16C3abD82fd1185482E01b49a"; -const VESTING_CATEGORY = "ecosystem_incentives_02"; +const VESTING_CATEGORY = "ecosystem_incentives_04"; type BalanceMap = { [key: string]: BigNumberish; From a70c90d68c1c995b11d3620a8d8a122be1434554 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Mon, 28 Oct 2024 10:37:20 +0000 Subject: [PATCH 16/74] Remove console logs from tests --- test/contracts/passport/PassportWalletRegistry.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/contracts/passport/PassportWalletRegistry.ts b/test/contracts/passport/PassportWalletRegistry.ts index 542fd97e..777459ea 100644 --- a/test/contracts/passport/PassportWalletRegistry.ts +++ b/test/contracts/passport/PassportWalletRegistry.ts @@ -73,8 +73,6 @@ describe("PassportWalletRegistry", () => { // Access the event logs for the "ScoreUpdated" event const event = receipt.events.find((e) => e.event === "WalletAdded"); - console.log(event); - if (!event || !event.args || event.args.length < 2) { throw new Error("WalletAdded event not found in the receipt"); } @@ -160,7 +158,6 @@ describe("PassportWalletRegistry", () => { const passportId = await passportRegistry.passportId(user1.address); - console.log(passportId); await passportWalletRegistry.connect(user1).addWallet(user2.address, passportId); const newWalletPassportId = await passportWalletRegistry.passportId(user2.address); From 45d761a238de5f8888beff7a55b7e32f0b9d5c2a Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Mon, 28 Oct 2024 12:44:18 +0200 Subject: [PATCH 17/74] Slight improvement on the constructor and some missing tests on it --- contracts/talent/TalentVault.sol | 14 +++++++----- test/contracts/talent/TalentVault.ts | 34 +++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 0a1e6375..a42649bc 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -7,6 +7,8 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "../passport/PassportBuilderScore.sol"; +error InvalidAddress(); + /// Based on WLDVault.sol from Worldcoin /// ref: https://optimistic.etherscan.io/address/0x21c4928109acb0659a88ae5329b5374a3024694c#code /// @title Talent Vault Contract @@ -98,11 +100,11 @@ contract TalentVault is Ownable, ReentrancyGuard { uint256 _maxYieldAmount, PassportBuilderScore _passportBuilderScore ) Ownable(msg.sender) { - require( - address(_token) != address(0) && - address(_yieldSource) != address(0), - "Invalid address" - ); + if (address(_token) == address(0) || + address(_yieldSource) == address(0) || + address(_passportBuilderScore) == address(0)) { + revert InvalidAddress(); + } token = _token; yieldRateBase = 10_00; @@ -320,4 +322,4 @@ contract TalentVault is Ownable, ReentrancyGuard { require(token.transferFrom(yieldSource, user, fromYieldSourceAmount), "Transfer failed"); } } -} \ No newline at end of file +} diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 407b783f..21b312b3 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -42,11 +42,12 @@ describe("TalentVault", () => { const totalAllowance = ethers.utils.parseUnits("600000000", 18); await talentToken.approve(talentVault.address, totalAllowance); await talentToken.unpause(); - await talentToken.renounceOwnership(); + // await talentToken.renounceOwnership(); }); describe("Deployment", () => { it("Should set the right owner", async () => { + expect(await talentVault.owner()).not.to.equal(ethers.constants.AddressZero); expect(await talentVault.owner()).to.equal(admin.address); }); @@ -55,7 +56,38 @@ describe("TalentVault", () => { expect(await talentVault.yieldRateProficient()).to.equal(15_00); expect(await talentVault.yieldRateCompetent()).to.equal(20_00); expect(await talentVault.yieldRateExpert()).to.equal(25_00); + expect(await talentVault.maxYieldAmount()).to.equal(ethers.utils.parseEther("500000")); + + expect(await talentVault.passportBuilderScore()).not.to.equal(ethers.constants.AddressZero); + expect(await talentVault.passportBuilderScore()).to.equal(passportBuilderScore.address); + }); + + it("reverts with InvalidAddress when _token given is 0", async () => { + await expect(deployContract(admin, Artifacts.TalentVault, [ + ethers.constants.AddressZero, + admin.address, + ethers.utils.parseEther("500000"), + passportBuilderScore.address, + ])).to.be.reverted; + }); + + it("reverts with InvalidAddress when _yieldSource given is 0", async () => { + await expect(deployContract(admin, Artifacts.TalentVault, [ + talentToken.address, + ethers.constants.AddressZero, + ethers.utils.parseEther("500000"), + passportBuilderScore.address, + ])).to.be.reverted; + }); + + it("reverts with InvalidAddress when _passportBuilderScore given is 0", async () => { + await expect(deployContract(admin, Artifacts.TalentVault, [ + talentToken.address, + admin.address, + ethers.utils.parseEther("500000"), + ethers.constants.AddressZero, + ])).to.be.reverted; }); }); From 124e23e073504d1872e65878dbaeaa554df6816d Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Mon, 28 Oct 2024 11:02:00 +0000 Subject: [PATCH 18/74] Add setup for main wallet configuration on mainnet --- .../passport/deployPassportWalletRegistry.ts | 28 +++++++++++++ scripts/passport/migrateMainWallets.ts | 42 +++++++++++++++++++ scripts/shared/index.ts | 13 ++++++ 3 files changed, 83 insertions(+) create mode 100644 scripts/passport/deployPassportWalletRegistry.ts create mode 100644 scripts/passport/migrateMainWallets.ts diff --git a/scripts/passport/deployPassportWalletRegistry.ts b/scripts/passport/deployPassportWalletRegistry.ts new file mode 100644 index 00000000..617ed3c3 --- /dev/null +++ b/scripts/passport/deployPassportWalletRegistry.ts @@ -0,0 +1,28 @@ +import { ethers, network } from "hardhat"; + +import { deployPassportWalletRegistry } from "../shared"; + +const PASSPORT_REGISTRY_ADDRESS_TESTNET = "0xa600b3356c1440B6D6e57b0B7862dC3dFB66bc43"; +const PASSPORT_REGISTRY_ADDRESS_MAINNET = "0xb477A9BD2547ad61f4Ac22113172Dd909E5B2331"; + +async function main() { + console.log(`Deploying passport registry at ${network.name}`); + + const [admin] = await ethers.getSigners(); + + console.log(`Admin will be ${admin.address}`); + + const passportRegistry = await deployPassportWalletRegistry(admin.address, PASSPORT_REGISTRY_ADDRESS_TESTNET); + + console.log(`Passport Registry Address: ${passportRegistry.address}`); + console.log(`Passport Registry owner: ${await passportRegistry.owner()}`); + + console.log("Done"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/passport/migrateMainWallets.ts b/scripts/passport/migrateMainWallets.ts new file mode 100644 index 00000000..7c5f160a --- /dev/null +++ b/scripts/passport/migrateMainWallets.ts @@ -0,0 +1,42 @@ +import { ethers, network } from "hardhat"; +import { PassportWalletRegistry } from "../../test/shared/artifacts"; +import MAIN_WALLET_CHANGES from "../data/main-wallet-changes.json"; + +async function main() { + const [admin] = await ethers.getSigners(); + + const passportWalletRegistry = new ethers.Contract( + "0xA380D6189b03d9C09534d7f1d2e2bD24678e12c5", + PassportWalletRegistry.abi, + admin + ); + + const data = MAIN_WALLET_CHANGES as { passport_id: number; main_wallet: string }[]; + + console.log("MIGRATING: ", data.length, "WALLETS"); + let i = 0; + + for (const item of data) { + i++; + console.log(`MIGRATING: ${i}/${data.length} - ${item.main_wallet} - ${item.passport_id}`); + const tx = await passportWalletRegistry.adminAddWallet(item.main_wallet, item.passport_id); + console.log(`TX included: https://basescan.org/tx/${tx.hash}`); + + const validationId = await passportWalletRegistry.passportId(item.main_wallet); + if (validationId.toString() !== item.passport_id.toString()) { + console.log("VALUES ARE NOT EQUAL: ", validationId.toString(), item.passport_id.toString()); + process.exit(1); + } else { + console.log("VALUES ARE EQUAL: ", validationId.toString(), item.passport_id.toString()); + } + await tx.wait(); + } + console.log("Done"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/shared/index.ts b/scripts/shared/index.ts index 0160ced3..2f7dd6dc 100644 --- a/scripts/shared/index.ts +++ b/scripts/shared/index.ts @@ -8,6 +8,7 @@ import type { TalentCommunitySale, TalentTGEUnlock, SmartBuilderScore, + PassportWalletRegistry, } from "../../typechain-types"; import { BigNumber } from "ethers"; import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; @@ -21,6 +22,18 @@ export async function deployPassport(owner: string): Promise { return deployedPassport as PassportRegistry; } +export async function deployPassportWalletRegistry( + owner: string, + passportRegistry: string +): Promise { + const passportWalletRegistryContract = await ethers.getContractFactory("PassportWalletRegistry"); + + const deployedPassportWalletRegistry = await passportWalletRegistryContract.deploy(owner, passportRegistry); + await deployedPassportWalletRegistry.deployed(); + + return deployedPassportWalletRegistry as PassportWalletRegistry; +} + export async function deployTalentToken(owner: string): Promise { const talentTokenContract = await ethers.getContractFactory("TalentProtocolToken"); From dd19b829b2d879cf144c5a2156236f7344c571e9 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Mon, 28 Oct 2024 11:56:51 +0000 Subject: [PATCH 19/74] Integrate main wallet changes to TalentRewardClaim --- contracts/talent/TalentRewardClaim.sol | 6 ++++- .../passport/deployPassportWalletRegistry.ts | 2 +- scripts/passport/migrateMainWallets.ts | 2 +- test/contracts/talent/TalentRewardClaim.ts | 27 ++++++++++++++++++- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/contracts/talent/TalentRewardClaim.sol b/contracts/talent/TalentRewardClaim.sol index 028c46ed..e3731bcd 100644 --- a/contracts/talent/TalentRewardClaim.sol +++ b/contracts/talent/TalentRewardClaim.sol @@ -7,6 +7,7 @@ import "@openzeppelin/contracts/utils/math/Math.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "./TalentProtocolToken.sol"; import "../passport/PassportBuilderScore.sol"; +import "../passport/PassportWalletRegistry.sol"; import "../merkle/MerkleProof.sol"; contract TalentRewardClaim is Ownable, ReentrancyGuard { @@ -14,6 +15,7 @@ contract TalentRewardClaim is Ownable, ReentrancyGuard { TalentProtocolToken public talentToken; PassportBuilderScore public passportBuilderScore; + PassportWalletRegistry public passportWalletRegistry; address public holdingWallet; uint256 public constant WEEKLY_CLAIM_AMOUNT = 2000 ether; uint256 public constant WEEK_DURATION = 7 days; @@ -36,6 +38,7 @@ contract TalentRewardClaim is Ownable, ReentrancyGuard { constructor( TalentProtocolToken _talentToken, PassportBuilderScore _passportBuilderScore, + PassportWalletRegistry _passportWalletRegistry, address _holdingWallet, address initialOwner, bytes32 _merkleRoot @@ -43,6 +46,7 @@ contract TalentRewardClaim is Ownable, ReentrancyGuard { merkleRoot = _merkleRoot; talentToken = _talentToken; passportBuilderScore = _passportBuilderScore; + passportWalletRegistry = _passportWalletRegistry; holdingWallet = _holdingWallet; } @@ -89,7 +93,7 @@ contract TalentRewardClaim is Ownable, ReentrancyGuard { UserInfo storage user = userInfo[msg.sender]; require(amountToClaim > 0, "No tokens owed"); - uint256 passportId = passportBuilderScore.passportRegistry().passportId(beneficiary); + uint256 passportId = passportWalletRegistry.passportId(beneficiary); uint256 builderScore = passportBuilderScore.getScore(passportId); uint256 claimMultiplier = (builderScore > 40) ? 5 : 1; diff --git a/scripts/passport/deployPassportWalletRegistry.ts b/scripts/passport/deployPassportWalletRegistry.ts index 617ed3c3..794e1ae2 100644 --- a/scripts/passport/deployPassportWalletRegistry.ts +++ b/scripts/passport/deployPassportWalletRegistry.ts @@ -12,7 +12,7 @@ async function main() { console.log(`Admin will be ${admin.address}`); - const passportRegistry = await deployPassportWalletRegistry(admin.address, PASSPORT_REGISTRY_ADDRESS_TESTNET); + const passportRegistry = await deployPassportWalletRegistry(admin.address, PASSPORT_REGISTRY_ADDRESS_MAINNET); console.log(`Passport Registry Address: ${passportRegistry.address}`); console.log(`Passport Registry owner: ${await passportRegistry.owner()}`); diff --git a/scripts/passport/migrateMainWallets.ts b/scripts/passport/migrateMainWallets.ts index 7c5f160a..f033ed60 100644 --- a/scripts/passport/migrateMainWallets.ts +++ b/scripts/passport/migrateMainWallets.ts @@ -6,7 +6,7 @@ async function main() { const [admin] = await ethers.getSigners(); const passportWalletRegistry = new ethers.Contract( - "0xA380D6189b03d9C09534d7f1d2e2bD24678e12c5", + "0x9B729d9fC43e3746855F7E02238FB3a2A20bD899", PassportWalletRegistry.abi, admin ); diff --git a/test/contracts/talent/TalentRewardClaim.ts b/test/contracts/talent/TalentRewardClaim.ts index 9a8f3930..4f30e019 100644 --- a/test/contracts/talent/TalentRewardClaim.ts +++ b/test/contracts/talent/TalentRewardClaim.ts @@ -8,6 +8,7 @@ import { TalentRewardClaim, PassportRegistry, PassportBuilderScore, + PassportWalletRegistry, } from "../../../typechain-types"; import { Artifacts } from "../../shared"; import generateMerkleTree from "../../../functions/generateMerkleTree"; @@ -30,6 +31,7 @@ describe("TalentRewardClaim", () => { let talentToken: TalentProtocolToken; let passportRegistry: PassportRegistry; let passportBuilderScore: PassportBuilderScore; + let passportWalletRegistry: PassportWalletRegistry; let talentRewardClaim: TalentRewardClaim; let merkleTree: StandardMerkleTree<(string | BigNumber)[]>; let currentTimestamp: number = Math.floor(Date.now() / 1000); @@ -43,6 +45,10 @@ describe("TalentRewardClaim", () => { passportRegistry.address, admin.address, ])) as PassportBuilderScore; + passportWalletRegistry = (await deployContract(admin, Artifacts.PassportWalletRegistry, [ + admin.address, + passportRegistry.address, + ])) as PassportWalletRegistry; merkleTree = generateMerkleTree({ [user1.address]: ethers.utils.parseUnits("10000", 18), @@ -52,6 +58,7 @@ describe("TalentRewardClaim", () => { talentRewardClaim = (await deployContract(admin, Artifacts.TalentRewardClaim, [ talentToken.address, passportBuilderScore.address, + passportWalletRegistry.address, admin.address, admin.address, merkleTree.root, @@ -99,11 +106,16 @@ describe("TalentRewardClaim", () => { describe("Claiming Tokens", () => { beforeEach(async () => { - const amounts = [ethers.utils.parseUnits("10000", 18), ethers.utils.parseUnits("20000", 18)]; + const amounts = [ + ethers.utils.parseUnits("10000", 18), + ethers.utils.parseUnits("20000", 18), + ethers.utils.parseUnits("30000", 18), + ]; merkleTree = generateMerkleTree({ [user1.address]: amounts[0], [user2.address]: amounts[1], + [user3.address]: amounts[2], }); await talentRewardClaim.setMerkleRoot(merkleTree.root); @@ -179,6 +191,19 @@ describe("TalentRewardClaim", () => { expect(await talentToken.balanceOf(user1.address)).to.equal(ethers.utils.parseUnits("10000", 18)); // 5x the weekly amount }); + it("Should allow users that changed their main wallet with a builder score above 40 to claim 5x tokens", async () => { + await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode + await passportRegistry.connect(user1).create("source1"); + + const passportId = await passportRegistry.passportId(user1.address); + await passportWalletRegistry.connect(user1).addWallet(user3.address, passportId); + await passportBuilderScore.setScore(passportId, 50); // Set builder score above 40 + + const proof1 = merkleTree.getProof([user3.address, ethers.utils.parseUnits("30000", 18)]); + await talentRewardClaim.connect(user3).claimTokens(proof1, ethers.utils.parseUnits("30000", 18)); + expect(await talentToken.balanceOf(user3.address)).to.equal(ethers.utils.parseUnits("10000", 18)); // 5x the weekly amount + }); + it("Should burn tokens if a user misses a claim", async () => { const initialBalance = await talentToken.balanceOf(admin.address); From 22794a727d5422ea4b20309fedb037a304871215 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Mon, 28 Oct 2024 14:46:27 +0000 Subject: [PATCH 20/74] Update Talent VAULT with more tests --- contracts/talent/TalentVault.sol | 3 +++ scripts/shared/index.ts | 16 ++++++++++++++++ test/contracts/talent/TalentVault.ts | 21 +++++++++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 0a1e6375..47771ec0 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -45,6 +45,7 @@ contract TalentVault is Ownable, ReentrancyGuard { uint256 amount; uint256 depositedAmount; uint256 lastInterestCalculation; + uint256 lastDepositTimestamp; address user; } @@ -133,6 +134,7 @@ contract TalentVault is Ownable, ReentrancyGuard { userDeposit.depositedAmount += amount; userDeposit.lastInterestCalculation = block.timestamp; userDeposit.user = account; + userDeposit.lastDepositTimestamp = block.timestamp; emit Deposited(account, amount); @@ -197,6 +199,7 @@ contract TalentVault is Ownable, ReentrancyGuard { /// @notice Update the yield rate for the contract /// @dev Can only be called by the owner function setYieldRate(uint256 _yieldRate) external onlyOwner { + require(_yieldRate < 100_00, "Yield rate cannot be greater than 100%"); require(_yieldRate > yieldRateBase, "Yield rate cannot be decreased"); yieldRateBase = _yieldRate; diff --git a/scripts/shared/index.ts b/scripts/shared/index.ts index 2f7dd6dc..5774889c 100644 --- a/scripts/shared/index.ts +++ b/scripts/shared/index.ts @@ -9,6 +9,7 @@ import type { TalentTGEUnlock, SmartBuilderScore, PassportWalletRegistry, + TalentTGEUnlockTimestamp, } from "../../typechain-types"; import { BigNumber } from "ethers"; import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; @@ -46,6 +47,7 @@ export async function deployTalentToken(owner: string): Promise { + const talentTGEUnlockWithTimestampsContract = await ethers.getContractFactory("TalentTGEUnlockTimestamp"); + + const deployedTGEUnlock = await talentTGEUnlockWithTimestampsContract.deploy(token, merkleTreeRoot, owner, timestamp); + await deployedTGEUnlock.deployed(); + return deployedTGEUnlock as TalentTGEUnlockTimestamp; +} diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 407b783f..8bce4d79 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -34,7 +34,7 @@ describe("TalentVault", () => { talentVault = (await deployContract(admin, Artifacts.TalentVault, [ talentToken.address, admin.address, - ethers.utils.parseEther("500000"), + ethers.utils.parseEther("10000"), passportBuilderScore.address, ])) as TalentVault; @@ -55,7 +55,7 @@ describe("TalentVault", () => { expect(await talentVault.yieldRateProficient()).to.equal(15_00); expect(await talentVault.yieldRateCompetent()).to.equal(20_00); expect(await talentVault.yieldRateExpert()).to.equal(25_00); - expect(await talentVault.maxYieldAmount()).to.equal(ethers.utils.parseEther("500000")); + expect(await talentVault.maxYieldAmount()).to.equal(ethers.utils.parseEther("10000")); }); }); @@ -118,6 +118,23 @@ describe("TalentVault", () => { expect(userBalance).to.equal(depositAmount.add(expectedInterest)); }); + // 10000 + it("Should calculate interest even if amount is above the max yield amount correctly", async () => { + const depositAmount = ethers.utils.parseEther("15000"); + const maxAmount = ethers.utils.parseEther("10000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount); + + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); + + const expectedInterest = maxAmount.mul(10).div(100); // 10% interest + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.equal(depositAmount.add(expectedInterest)); + }); + it("Should calculate interest correctly for builders with scores below 50", async () => { await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode await passportRegistry.connect(user1).create("source1"); From d68554cade409fe984209b6262f0bc10397d2227 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Mon, 28 Oct 2024 17:47:35 +0200 Subject: [PATCH 21/74] WiP --- contracts/talent/TalentVault.sol | 52 ++++++++++--- test/contracts/talent/TalentVault.ts | 108 +++++++++++++++++++++++++-- 2 files changed, 144 insertions(+), 16 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 1e6bcab7..47862722 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -8,6 +8,12 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "../passport/PassportBuilderScore.sol"; error InvalidAddress(); +error InvalidDepositAmount(); +error InsufficientBalance(); +error InsufficientAllowance(); +error TransferFailed(); +error NoDepositFound(); +error ContractInsolvent(); /// Based on WLDVault.sol from Worldcoin /// ref: https://optimistic.etherscan.io/address/0x21c4928109acb0659a88ae5329b5374a3024694c#code @@ -122,9 +128,24 @@ contract TalentVault is Ownable, ReentrancyGuard { /// @param account The address of the user to deposit tokens for /// @param amount The amount of tokens to deposit function depositForAddress(address account, uint256 amount) public { - require(amount > 0, "Invalid deposit amount"); - require(token.balanceOf(msg.sender) >= amount, "Insufficient balance"); - require(token.allowance(msg.sender, address(this)) >= amount, "Insufficient allowance"); + if (amount <= 0) { + revert InvalidDepositAmount(); + } + + if (token.balanceOf(msg.sender) < amount) { + revert InsufficientBalance(); + } + + if (token.allowance(msg.sender, address(this)) < amount) { + revert InsufficientAllowance(); + } + + try token.transferFrom(msg.sender, address(this), amount) { + // Transfer was successful; no further action needed + } catch { + // If the transfer failed, revert with a custom error message + revert TransferFailed(); + } Deposit storage userDeposit = getDeposit[account]; @@ -139,8 +160,6 @@ contract TalentVault is Ownable, ReentrancyGuard { userDeposit.user = account; emit Deposited(account, amount); - - require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); } /// @notice Deposit tokens into the contract, which will start accruing interest. @@ -153,7 +172,10 @@ contract TalentVault is Ownable, ReentrancyGuard { /// @param account The address of the user to refresh function refreshForAddress(address account) public { Deposit storage userDeposit = getDeposit[account]; - require(userDeposit.amount > 0, "No deposit found"); + if (userDeposit.amount <= 0) { + revert NoDepositFound(); + } + refreshInterest(userDeposit); } @@ -185,7 +207,9 @@ contract TalentVault is Ownable, ReentrancyGuard { function recoverDeposit() external { Deposit storage userDeposit = getDeposit[msg.sender]; - require(userDeposit.amount > 0, "No deposit found"); + if (userDeposit.amount <= 0) { + revert NoDepositFound(); + } refreshInterest(userDeposit); uint256 amount = userDeposit.depositedAmount; @@ -193,9 +217,15 @@ contract TalentVault is Ownable, ReentrancyGuard { userDeposit.amount -= amount; userDeposit.depositedAmount = 0; + if (token.balanceOf(address(this)) < amount) { + revert ContractInsolvent(); + } + + try token.transfer(msg.sender, amount) {} catch { + revert TransferFailed(); + } + emit Withdrawn(msg.sender, amount); - require(token.balanceOf(address(this)) >= amount, "Contract insolvent"); - require(token.transfer(msg.sender, amount), "Transfer failed"); } /// @notice Update the yield rate for the contract @@ -292,7 +322,9 @@ contract TalentVault is Ownable, ReentrancyGuard { /// @param amount The amount of tokens to withdraw function _withdraw(address user, uint256 amount) internal { Deposit storage userDeposit = getDeposit[user]; - require(userDeposit.amount > 0, "No deposit found"); + if (userDeposit.amount <= 0) { + revert NoDepositFound(); + } refreshInterest(userDeposit); require(userDeposit.amount >= amount, "Not enough balance"); diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 22cb0bcb..2f65d449 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -5,6 +5,7 @@ import { solidity } from "ethereum-waffle"; import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { TalentProtocolToken, TalentVault, PassportRegistry, PassportBuilderScore } from "../../../typechain-types"; import { Artifacts } from "../../shared"; +import { TalentVault as TalentVaultArtifact } from "../../shared/artifacts"; chai.use(solidity); @@ -99,20 +100,115 @@ describe("TalentVault", () => { describe("Deposits", () => { it("Should allow users to deposit tokens", async () => { - const depositAmount = ethers.utils.parseEther("1000"); - await talentToken.transfer(user1.address, depositAmount); + const depositAmount = 10_000n; + + await talentToken.transfer(user1.address, depositAmount); // so that it has enough balance + const user1BalanceBefore = await talentToken.balanceOf(user1.address); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + const vaultBalanceBefore = await talentToken.balanceOf(talentVault.address); + + const user1BalanceInTalentVaultBefore = await talentVault.balanceOf(user1.address); + + // fire await expect(talentVault.connect(user1).deposit(depositAmount)) .to.emit(talentVault, "Deposited") .withArgs(user1.address, depositAmount); - const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.equal(depositAmount); + // vault balance in TALENT is increased + const vaultBalanceAfter = await talentToken.balanceOf(talentVault.address); + const expectedVaultBalanceAfter = vaultBalanceBefore.toBigInt() + depositAmount; + expect(vaultBalanceAfter).to.equal(expectedVaultBalanceAfter); + + // user1 balance in TALENT decreases + const user1BalanceAfter = await talentToken.balanceOf(user1.address); + expect(user1BalanceAfter).to.equal(user1BalanceBefore.toBigInt() - depositAmount); + + // user1 balance in TalentVault increases + const user1BalanceInTalentVaultAfter = await talentVault.balanceOf(user1.address); + expect(user1BalanceInTalentVaultAfter).to.equal(user1BalanceInTalentVaultBefore.toBigInt() + depositAmount); }); - it("Should not allow deposits of zero tokens", async () => { - await expect(talentVault.connect(user1).deposit(0)).to.be.revertedWith("Invalid deposit amount"); + describe("#depositForAddress", async () => { + it("Should deposit the amount to the address given", async () => { + const depositAmount = 100_000n; + await talentToken.transfer(user1.address, depositAmount); // so that sender has enough balance + const user1BalanceBefore = await talentToken.balanceOf(user1.address); + + await talentToken.connect(user1).approve(talentVault.address, depositAmount); // so that sender has approved vault + + const vaultBalanceBefore = await talentToken.balanceOf(talentVault.address); + + const user2DepositBefore = await talentVault.getDeposit(user2.address); + + // fire + await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)) + .to.emit(talentVault, "Deposited") + .withArgs(user2.address, depositAmount); + + // user1 talent balance is decreased + const user1BalanceAfter = await talentVault.balanceOf(user1.address); + const expectedUser1BalanceAfter = user1BalanceBefore.sub(depositAmount); + expect(user1BalanceAfter).to.equal(expectedUser1BalanceAfter); + + // vault balance is increased + const vaultBalanceAfter = await talentToken.balanceOf(talentVault.address); + const expectedVaultBalanceAfter = vaultBalanceBefore.toBigInt() + depositAmount; + expect(vaultBalanceAfter).to.equal(expectedVaultBalanceAfter); + + // deposit for user2 is updated on storage + const user2DepositAfter = await talentVault.getDeposit(user2.address); + expect(user2DepositAfter.user).to.equal(user2.address); + expect(user2DepositAfter.depositedAmount).to.equal( + user2DepositBefore.depositedAmount.toBigInt() + depositAmount + ); + expect(user2DepositAfter.amount).to.equal(user2DepositBefore.amount.toBigInt() + depositAmount); + }); + + it("Should not allow deposits of zero tokens", async () => { + await expect(talentVault.connect(user1).depositForAddress(ethers.constants.AddressZero, 0n)).to.be.revertedWith( + "InvalidDepositAmount()" + ); + }); + + it("Should not allow deposit of amount that the sender does not have", async () => { + const balanceOfUser1 = 100_000n; + + await talentToken.transfer(user1.address, balanceOfUser1); + + const depositAmount = 100_001n; + + await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)).to.be.revertedWith( + "InsufficientBalance()" + ); + }); + + it("Should not allow deposit of amount bigger than the allowed by the sender to be spent by the talent contract", async () => { + const depositAmount = 100_000n; + + await talentToken.transfer(user1.address, depositAmount); // so that user1 has enough balance + + const approvedAmount = depositAmount - 1n; + + await talentToken.connect(user1).approve(talentVault.address, approvedAmount); + + // fire + + await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)).to.be.revertedWith( + "InsufficientAllowance()" + ); + }); + + it("Should allow deposit of amount equal to the allowed by the sender to be spent by the talent contract", async () => { + const depositAmount = ethers.utils.parseEther("1000"); + + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + + await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)).to.be.revertedWith( + "InsufficientBalance()" + ); + }); }); }); From d6a101ff8a869b95a86b78181970960691e7abff Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Tue, 29 Oct 2024 19:25:22 +0200 Subject: [PATCH 22/74] Constructor allows some initial balance on the owner --- contracts/talent/TalentVault.sol | 5 +++-- test/contracts/talent/TalentVault.ts | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 47862722..4caf5990 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -104,8 +104,9 @@ contract TalentVault is Ownable, ReentrancyGuard { IERC20 _token, address _yieldSource, uint256 _maxYieldAmount, - PassportBuilderScore _passportBuilderScore - ) Ownable(msg.sender) { + PassportBuilderScore _passportBuilderScore, + uint256 _initialOwnerBalance + ) ERC4626(_token) ERC20("TalentVault", "TALENTVAULT") Ownable(msg.sender) { if ( address(_token) == address(0) || address(_yieldSource) == address(0) || diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 19c25d72..3cca46ce 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -37,6 +37,7 @@ describe("TalentVault", () => { admin.address, ethers.utils.parseEther("10000"), passportBuilderScore.address, + ethers.utils.parseEther("10000"), ])) as TalentVault; // Approve TalentVault contract to spend tokens on behalf of the admin @@ -52,6 +53,11 @@ describe("TalentVault", () => { expect(await talentVault.owner()).to.equal(admin.address); }); + it("Should set some initial balance for the owner", async () => { + const ownerBalance = await talentVault.balanceOf(admin.address); + expect(ownerBalance).to.equal(ethers.utils.parseEther("10000")); + }); + it("Should set the correct initial values", async () => { expect(await talentVault.yieldRateBase()).to.equal(10_00); expect(await talentVault.yieldRateProficient()).to.equal(15_00); From 500f59ccedfaeb5e0d77f4376f7228f130700326 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Tue, 29 Oct 2024 19:26:46 +0200 Subject: [PATCH 23/74] Transferrability is switched off for TalentVault and Deposit struct renamed to UserDeposit because there was an event 'Deposit' already defined in ERC4626 --- contracts/talent/TalentVault.sol | 51 ++++++++++++++++++---------- test/contracts/talent/TalentVault.ts | 18 ++++++++++ 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 4caf5990..58ac1c58 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -2,25 +2,27 @@ pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "../passport/PassportBuilderScore.sol"; +error ContractInsolvent(); +error InsufficientAllowance(); +error InsufficientBalance(); error InvalidAddress(); error InvalidDepositAmount(); -error InsufficientBalance(); -error InsufficientAllowance(); -error TransferFailed(); error NoDepositFound(); -error ContractInsolvent(); +error TalentVaultNonTransferable(); +error TransferFailed(); /// Based on WLDVault.sol from Worldcoin /// ref: https://optimistic.etherscan.io/address/0x21c4928109acb0659a88ae5329b5374a3024694c#code /// @title Talent Vault Contract /// @author Francisco Leal /// @notice Allows any $TALENT holders to deposit their tokens and earn interest. -contract TalentVault is Ownable, ReentrancyGuard { +contract TalentVault is ERC4626, Ownable, ReentrancyGuard { using SafeERC20 for IERC20; /// @notice Emitted when a user deposits tokens @@ -49,7 +51,7 @@ contract TalentVault is Ownable, ReentrancyGuard { /// @param amount The amount of tokens deposited, plus any accrued interest /// @param depositedAmount The amount of tokens that were deposited, excluding interest /// @param lastInterestCalculation The timestamp of the last interest calculation for this deposit - struct Deposit { + struct UserDeposit { uint256 amount; uint256 depositedAmount; uint256 lastInterestCalculation; @@ -93,7 +95,7 @@ contract TalentVault is Ownable, ReentrancyGuard { PassportBuilderScore public passportBuilderScore; /// @notice A mapping of user addresses to their deposits - mapping(address => Deposit) public getDeposit; + mapping(address => UserDeposit) public getDeposit; /// @notice Create a new Talent Vault contract /// @param _token The token that will be deposited into the contract @@ -123,9 +125,10 @@ contract TalentVault is Ownable, ReentrancyGuard { yieldSource = _yieldSource; maxYieldAmount = _maxYieldAmount; passportBuilderScore = _passportBuilderScore; + _mint(owner(), _initialOwnerBalance); } - /// @notice Deposit tokens into a user's account, which will start accruing interest. + /// @notice UserDeposit tokens into a user's account, which will start accruing interest. /// @param account The address of the user to deposit tokens for /// @param amount The amount of tokens to deposit function depositForAddress(address account, uint256 amount) public { @@ -148,7 +151,7 @@ contract TalentVault is Ownable, ReentrancyGuard { revert TransferFailed(); } - Deposit storage userDeposit = getDeposit[account]; + UserDeposit storage userDeposit = getDeposit[account]; if (userDeposit.amount > 0) { uint256 interest = calculateInterest(userDeposit); @@ -163,7 +166,7 @@ contract TalentVault is Ownable, ReentrancyGuard { emit Deposited(account, amount); } - /// @notice Deposit tokens into the contract, which will start accruing interest. + /// @notice UserDeposit tokens into the contract, which will start accruing interest. /// @param amount The amount of tokens to deposit function deposit(uint256 amount) public { depositForAddress(msg.sender, amount); @@ -172,7 +175,7 @@ contract TalentVault is Ownable, ReentrancyGuard { /// @notice Calculate any accrued interest. /// @param account The address of the user to refresh function refreshForAddress(address account) public { - Deposit storage userDeposit = getDeposit[account]; + UserDeposit storage userDeposit = getDeposit[account]; if (userDeposit.amount <= 0) { revert NoDepositFound(); } @@ -187,8 +190,10 @@ contract TalentVault is Ownable, ReentrancyGuard { /// @notice Returns the balance of the user, including any accrued interest. /// @param user The address of the user to check the balance of - function balanceOf(address user) public view returns (uint256) { - Deposit storage userDeposit = getDeposit[user]; + function balanceOf(address user) public view virtual override(ERC20, IERC20) returns (uint256) { + return super.balanceOf(user); + + UserDeposit storage userDeposit = getDeposit[user]; if (userDeposit.amount == 0) return 0; uint256 interest = calculateInterest(userDeposit); @@ -207,7 +212,7 @@ contract TalentVault is Ownable, ReentrancyGuard { } function recoverDeposit() external { - Deposit storage userDeposit = getDeposit[msg.sender]; + UserDeposit storage userDeposit = getDeposit[msg.sender]; if (userDeposit.amount <= 0) { revert NoDepositFound(); } @@ -280,10 +285,22 @@ contract TalentVault is Ownable, ReentrancyGuard { passportBuilderScore = _passportBuilderScore; } + /// @notice This reverts because TalentVault is non-transferable + /// @dev reverts with TalentVaultNonTransferable + function transfer(address, uint256) public virtual override(ERC20, IERC20) returns (bool) { + revert TalentVaultNonTransferable(); + } + + /// @notice This reverts because TalentVault is non-transferable + /// @dev reverts with TalentVaultNonTansferable + function transferFrom(address, address, uint256) public virtual override(ERC20, IERC20) returns (bool) { + revert TalentVaultNonTransferable(); + } + /// @dev Calculates the interest accrued on the deposit /// @param userDeposit The user's deposit /// @return The amount of interest accrued - function calculateInterest(Deposit memory userDeposit) internal view returns (uint256) { + function calculateInterest(UserDeposit memory userDeposit) internal view returns (uint256) { if (userDeposit.amount > maxYieldAmount) { userDeposit.amount = maxYieldAmount; } @@ -310,7 +327,7 @@ contract TalentVault is Ownable, ReentrancyGuard { /// @dev Refreshes the interest on a user's deposit /// @param userDeposit The user's deposit - function refreshInterest(Deposit storage userDeposit) internal { + function refreshInterest(UserDeposit storage userDeposit) internal { if (userDeposit.amount == 0) return; uint256 interest = calculateInterest(userDeposit); @@ -322,7 +339,7 @@ contract TalentVault is Ownable, ReentrancyGuard { /// @param user The address of the user to withdraw the balance of /// @param amount The amount of tokens to withdraw function _withdraw(address user, uint256 amount) internal { - Deposit storage userDeposit = getDeposit[user]; + UserDeposit storage userDeposit = getDeposit[user]; if (userDeposit.amount <= 0) { revert NoDepositFound(); } diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 3cca46ce..58e73a55 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -104,6 +104,24 @@ describe("TalentVault", () => { }); }); + describe("Transferability", async () => { + describe("#transfer", async () => { + it("reverts because TalentVault is not transferable", async () => { + await expect(talentVault.transfer(user1.address, 10n)).to.be.revertedWith("TalentVaultNonTransferable"); + }); + }); + + describe("#transferFrom", async () => { + it("reverts because TalentVault is not transferable", async () => { + await talentVault.approve(admin.address, 10n); + // fire + await expect(talentVault.transferFrom(admin.address, user2.address, 10n)).to.be.revertedWith( + "TalentVaultNonTransferable" + ); + }); + }); + }); + describe("Deposits", () => { it("Should allow users to deposit tokens", async () => { const depositAmount = 10_000n; From fcefc39492ddebb346209cc102c6eaef453ebf41 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Tue, 29 Oct 2024 19:35:04 +0200 Subject: [PATCH 24/74] Name and Symbol SHOULD reflect the underlying token's name and symbol --- contracts/talent/TalentVault.sol | 2 +- test/contracts/talent/TalentVault.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 58ac1c58..dc8ae19f 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -108,7 +108,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { uint256 _maxYieldAmount, PassportBuilderScore _passportBuilderScore, uint256 _initialOwnerBalance - ) ERC4626(_token) ERC20("TalentVault", "TALENTVAULT") Ownable(msg.sender) { + ) ERC4626(_token) ERC20("TalentProtocolVaultToken", "TALENTVAULT") Ownable(msg.sender) { if ( address(_token) == address(0) || address(_yieldSource) == address(0) || diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 58e73a55..2536eca1 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -104,6 +104,22 @@ describe("TalentVault", () => { }); }); + describe("#name", async () => { + it("is 'TalentProtocolVaultToken' reflects the underlying token name, i.e. of 'TalentProtocolToken'", async () => { + const name = await talentVault.name(); + + expect(name).to.equal("TalentProtocolVaultToken"); + }); + }); + + describe("#symbol", async () => { + it("is 'TALENTVAULT' reflects the underlying token symbol, i.e. of 'TALENT'", async () => { + const symbol = await talentVault.symbol(); + + expect(symbol).to.equal("TALENTVAULT"); + }); + }); + describe("Transferability", async () => { describe("#transfer", async () => { it("reverts because TalentVault is not transferable", async () => { From aa3eb65e7d98d06d6b013b2f5733f1b5dbec7d63 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Tue, 29 Oct 2024 19:44:16 +0200 Subject: [PATCH 25/74] Test that talentVault#asset returns the address of the contract --- test/contracts/talent/TalentVault.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 2536eca1..f80fdd3b 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -120,6 +120,15 @@ describe("TalentVault", () => { }); }); + describe("#asset", async () => { + it("returns the address of the $TALENT contract", async () => { + const returnedAddress = await talentVault.asset(); + + expect(returnedAddress).not.to.equal(ethers.constants.AddressZero); + expect(returnedAddress).to.equal(talentToken.address); + }); + }); + describe("Transferability", async () => { describe("#transfer", async () => { it("reverts because TalentVault is not transferable", async () => { From 27656e33656f790e6839f925f6eb882ebbc92f9d Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Tue, 29 Oct 2024 20:39:49 +0200 Subject: [PATCH 26/74] TalentVault#totalAccess() implemented --- contracts/talent/TalentVault.sol | 12 ++++++++---- test/contracts/talent/TalentVault.ts | 11 +++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index dc8ae19f..c4375eb4 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -166,10 +166,14 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { emit Deposited(account, amount); } - /// @notice UserDeposit tokens into the contract, which will start accruing interest. - /// @param amount The amount of tokens to deposit - function deposit(uint256 amount) public { - depositForAddress(msg.sender, amount); + // /// @notice UserDeposit tokens into the contract, which will start accruing interest. + // /// @param amount The amount of tokens to deposit + // function deposit(uint256 amount) public { + // depositForAddress(msg.sender, amount); + // } + + function deposit(uint256 assets, address receiver) public virtual override returns (uint256) { + depositForAddress(receiver, assets); } /// @notice Calculate any accrued interest. diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index f80fdd3b..1587f47b 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -129,6 +129,17 @@ describe("TalentVault", () => { }); }); + describe("#totalAssets", async () => { + it("returns the number of $TALENT that TalentVault Contract has as balance", async () => { + await talentVault.deposit(10n, user1.address); + + const returnedValue = await talentVault.totalAssets(); + const balanceOfTalentVaultInTalent = await talentToken.balanceOf(talentVault.address); + + expect(returnedValue).to.equal(balanceOfTalentVaultInTalent); + }); + }); + describe("Transferability", async () => { describe("#transfer", async () => { it("reverts because TalentVault is not transferable", async () => { From d8375f08b0e5f82b9a7a6f06f227e137adf6135f Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Tue, 29 Oct 2024 21:12:19 +0200 Subject: [PATCH 27/74] TalentVault#setMaxDeposit,#maxDeposit --- contracts/talent/TalentVault.sol | 10 +++++++++ test/contracts/talent/TalentVault.ts | 31 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index c4375eb4..4f0fda8f 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -97,6 +97,8 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice A mapping of user addresses to their deposits mapping(address => UserDeposit) public getDeposit; + mapping(address => uint256) private maxDeposits; + /// @notice Create a new Talent Vault contract /// @param _token The token that will be deposited into the contract /// @param _yieldSource The wallet paying for the yield @@ -172,6 +174,14 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { // depositForAddress(msg.sender, amount); // } + function setMaxDeposit(address receiver, uint256 assets) public onlyOwner { + maxDeposits[receiver] = assets; + } + + function maxDeposit(address receiver) public view virtual override returns (uint256) { + return maxDeposits[receiver]; + } + function deposit(uint256 assets, address receiver) public virtual override returns (uint256) { depositForAddress(receiver, assets); } diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 1587f47b..132c471b 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -139,6 +139,37 @@ describe("TalentVault", () => { expect(returnedValue).to.equal(balanceOfTalentVaultInTalent); }); }); + describe("#setMaxDeposit", async () => { + context("when called by the owner", async () => { + it("sets the maximum deposit for the receiver", async () => { + await talentVault.setMaxDeposit(user1.address, 10n); + + const deposit = await talentVault.maxDeposit(user1.address); + + expect(deposit).to.equal(10n); + }); + }); + + context("when called by a non-owner", async () => { + it("reverts", async () => { + await expect(talentVault.connect(user1).setMaxDeposit(user2.address, 10n)).to.revertedWith( + "OwnableUnauthorizedAccount" + ); + }); + }); + }); + + describe("#maxDeposit", async () => { + context("when recipient has positive maximum deposit limit", async () => { + it("returns it", async () => { + await talentVault.setMaxDeposit(user1.address, 5n); + + const maxDeposit = await talentVault.maxDeposit(user1.address); + + expect(maxDeposit).to.equal(5n); + }); + }); + }); describe("Transferability", async () => { describe("#transfer", async () => { From fdd788f8a35237cc4bd3a90ddd9cbfe121b52f2b Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Tue, 29 Oct 2024 21:12:57 +0200 Subject: [PATCH 28/74] TODO Note whether we want the smart contract to be Pausable --- test/contracts/talent/TalentVault.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 132c471b..7773b4e9 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -104,6 +104,19 @@ describe("TalentVault", () => { }); }); + // TODO: Do we want this? Does it + // depreciate the reliability of the contract at + // the eyes of our users? + // + describe("Pausable", async () => { + it("is Pausable"); + it("when Pausable, we cannot deposit"); + it("when Pausable, we cannot withdraw"); + it("when paused we cannot .... other ..."); + it("can be paused only by the owner"); + it("can be unpaused only by the owner"); + }); + describe("#name", async () => { it("is 'TalentProtocolVaultToken' reflects the underlying token name, i.e. of 'TalentProtocolToken'", async () => { const name = await talentVault.name(); From fb315375e6aa33b1b81c7029ba7eb78b7c35dbdc Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Wed, 30 Oct 2024 12:52:31 +0200 Subject: [PATCH 29/74] WIP to make it ERC4626 --- contracts/talent/TalentVault.sol | 254 +++++++------- test/contracts/talent/TalentVault.ts | 498 ++++++++++++++++++++++----- 2 files changed, 548 insertions(+), 204 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 4f0fda8f..632ad216 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -25,15 +25,18 @@ error TransferFailed(); contract TalentVault is ERC4626, Ownable, ReentrancyGuard { using SafeERC20 for IERC20; - /// @notice Emitted when a user deposits tokens - /// @param user The address of the user who deposited tokens - /// @param amount The amount of tokens deposited - event Deposited(address indexed user, uint256 amount); + // This is not needed, since ERC4626 already emits + // event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares) - /// @notice Emitted when a user withdraws tokens - /// @param user The address of the user who withdrew tokens - /// @param amount The amount of tokens withdrawn - event Withdrawn(address indexed user, uint256 amount); + // /// @notice Emitted when a user deposits tokens + // /// @param user The address of the user who deposited tokens + // /// @param amount The amount of tokens deposited + // event Deposited(address indexed user, uint256 amount); + + // /// @notice Emitted when a user withdraws tokens + // /// @param user The address of the user who withdrew tokens + // /// @param amount The amount of tokens withdrawn + // event Withdrawn(address indexed user, uint256 amount); /// @notice Emitted when the yield rate is updated /// @param yieldRate The new yield rate @@ -48,11 +51,9 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { event YieldAccrualDeadlineUpdated(uint256 yieldAccrualDeadline); /// @notice Represents a user's deposit - /// @param amount The amount of tokens deposited, plus any accrued interest /// @param depositedAmount The amount of tokens that were deposited, excluding interest /// @param lastInterestCalculation The timestamp of the last interest calculation for this deposit struct UserDeposit { - uint256 amount; uint256 depositedAmount; uint256 lastInterestCalculation; address user; @@ -97,6 +98,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice A mapping of user addresses to their deposits mapping(address => UserDeposit) public getDeposit; + mapping(address => bool) private maxDepositLimitFlags; mapping(address => uint256) private maxDeposits; /// @notice Create a new Talent Vault contract @@ -108,9 +110,13 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { IERC20 _token, address _yieldSource, uint256 _maxYieldAmount, - PassportBuilderScore _passportBuilderScore, - uint256 _initialOwnerBalance - ) ERC4626(_token) ERC20("TalentProtocolVaultToken", "TALENTVAULT") Ownable(msg.sender) { + PassportBuilderScore _passportBuilderScore + ) + // uint256 _initialOwnerBalance // added this for the needs to test + ERC4626(_token) + ERC20("TalentProtocolVaultToken", "TALENTVAULT") + Ownable(msg.sender) + { if ( address(_token) == address(0) || address(_yieldSource) == address(0) || @@ -127,70 +133,91 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { yieldSource = _yieldSource; maxYieldAmount = _maxYieldAmount; passportBuilderScore = _passportBuilderScore; - _mint(owner(), _initialOwnerBalance); } - /// @notice UserDeposit tokens into a user's account, which will start accruing interest. - /// @param account The address of the user to deposit tokens for - /// @param amount The amount of tokens to deposit - function depositForAddress(address account, uint256 amount) public { - if (amount <= 0) { - revert InvalidDepositAmount(); - } + // /// @notice UserDeposit tokens into the contract, which will start accruing interest. + // /// @param amount The amount of tokens to deposit + // function deposit(uint256 amount) public { + // depositForAddress(msg.sender, amount); + // } - if (token.balanceOf(msg.sender) < amount) { - revert InsufficientBalance(); - } + function setMaxDeposit(address receiver, uint256 assets) public onlyOwner { + maxDeposits[receiver] = assets; + maxDepositLimitFlags[receiver] = true; + } - if (token.allowance(msg.sender, address(this)) < amount) { - revert InsufficientAllowance(); - } + function setMaxMint(address receiver, uint256 shares) public onlyOwner { + setMaxDeposit(receiver, shares); + } + + function removeMaxDepositLimit(address receiver) public onlyOwner { + delete maxDeposits[receiver]; + delete maxDepositLimitFlags[receiver]; + } + + function removeMaxMintLimit(address receiver) public onlyOwner { + removeMaxDepositLimit(receiver); + } - try token.transferFrom(msg.sender, address(this), amount) { - // Transfer was successful; no further action needed - } catch { - // If the transfer failed, revert with a custom error message - revert TransferFailed(); + function maxDeposit(address receiver) public view virtual override returns (uint256) { + if (maxDepositLimitFlags[receiver]) { + return maxDeposits[receiver]; + } else { + return type(uint256).max; } + } - UserDeposit storage userDeposit = getDeposit[account]; + // @dev We consider +shares+ and +assets+ to have 1-to-1 equivalence + // Hence, deposits and mints are treated equally + function maxMint(address receiver) public view virtual override returns (uint256) { + return maxDeposit(receiver); + } - if (userDeposit.amount > 0) { - uint256 interest = calculateInterest(userDeposit); - userDeposit.amount += interest; + function deposit(uint256 assets, address receiver) public virtual override returns (uint256) { + if (assets <= 0) { + revert InvalidDepositAmount(); } - userDeposit.amount += amount; - userDeposit.depositedAmount += amount; - userDeposit.lastInterestCalculation = block.timestamp; - userDeposit.user = account; + UserDeposit storage userDeposit = getDeposit[receiver]; - emit Deposited(account, amount); - } + // This we don't need it only for reporting purposes. + // Note that the +assets+ is $TALENT that is moved from + // +receiver+ wallet to +address(this)+ wallet. + // But this one here, it gives us an easy way to find out + // how much $TALENT a +receiver+ has deposited. + userDeposit.depositedAmount += assets; - // /// @notice UserDeposit tokens into the contract, which will start accruing interest. - // /// @param amount The amount of tokens to deposit - // function deposit(uint256 amount) public { - // depositForAddress(msg.sender, amount); - // } + userDeposit.user = receiver; // Why do we need this? - function setMaxDeposit(address receiver, uint256 assets) public onlyOwner { - maxDeposits[receiver] = assets; + uint256 balanceOfReceiverBeforeDeposit = balanceOf(receiver); + + uint256 shares = super.deposit(assets, receiver); + + if (balanceOfReceiverBeforeDeposit > 0) { + uint256 interest = calculateInterest(balanceOfReceiverBeforeDeposit, userDeposit); + userDeposit.lastInterestCalculation = block.timestamp; + _deposit(yieldSource, receiver, interest, interest); + } + + return shares; } - function maxDeposit(address receiver) public view virtual override returns (uint256) { - return maxDeposits[receiver]; + /// @notice UserDeposit tokens into a user's account, which will start accruing interest. + /// @param account The address of the user to deposit tokens fo + /// @param amount The amount of tokens to deposit + function depositForAddress(address account, uint256 amount) public { + deposit(amount, account); } - function deposit(uint256 assets, address receiver) public virtual override returns (uint256) { - depositForAddress(receiver, assets); + function mint(uint256 shares, address receiver) public virtual override returns (uint256) { + return deposit(shares, receiver); } /// @notice Calculate any accrued interest. /// @param account The address of the user to refresh function refreshForAddress(address account) public { UserDeposit storage userDeposit = getDeposit[account]; - if (userDeposit.amount <= 0) { + if (balanceOf(account) <= 0) { revert NoDepositFound(); } @@ -207,18 +234,26 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { function balanceOf(address user) public view virtual override(ERC20, IERC20) returns (uint256) { return super.balanceOf(user); - UserDeposit storage userDeposit = getDeposit[user]; - if (userDeposit.amount == 0) return 0; + // UserDeposit storage userDeposit = getDeposit[user]; + // if (balanceOf(user) == 0) return 0; - uint256 interest = calculateInterest(userDeposit); + // uint256 interest = calculateInterest(userDeposit); - return userDeposit.amount + interest; + // return userDeposit.amount + interest; } /// @notice Withdraws the requested amount from the user's balance. - function withdraw(uint256 amount) external nonReentrant { - _withdraw(msg.sender, amount); - } + // function withdraw(uint256 amount) external nonReentrant { + // _withdraw(msg.sender, amount); + // } + + // function withdraw( + // uint256 assets, + // address receiver, + // address owner + // ) public virtual override returns (uint256 shares) { + // return super.withdraw(assets, receiver, owner); + // } /// @notice Withdraws all of the user's balance, including any accrued interest. function withdrawAll() external nonReentrant { @@ -226,26 +261,21 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { } function recoverDeposit() external { - UserDeposit storage userDeposit = getDeposit[msg.sender]; - if (userDeposit.amount <= 0) { - revert NoDepositFound(); - } - - refreshInterest(userDeposit); - uint256 amount = userDeposit.depositedAmount; - - userDeposit.amount -= amount; - userDeposit.depositedAmount = 0; - - if (token.balanceOf(address(this)) < amount) { - revert ContractInsolvent(); - } - - try token.transfer(msg.sender, amount) {} catch { - revert TransferFailed(); - } - - emit Withdrawn(msg.sender, amount); + // UserDeposit storage userDeposit = getDeposit[msg.sender]; + // if (userDeposit.amount <= 0) { + // revert NoDepositFound(); + // } + // refreshInterest(userDeposit); + // uint256 amount = userDeposit.depositedAmount; + // userDeposit.amount -= amount; + // userDeposit.depositedAmount = 0; + // if (token.balanceOf(address(this)) < amount) { + // revert ContractInsolvent(); + // } + // try token.transfer(msg.sender, amount) {} catch { + // revert TransferFailed(); + // } + // emit Withdrawn(msg.sender, amount); } /// @notice Update the yield rate for the contract @@ -314,9 +344,9 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @dev Calculates the interest accrued on the deposit /// @param userDeposit The user's deposit /// @return The amount of interest accrued - function calculateInterest(UserDeposit memory userDeposit) internal view returns (uint256) { - if (userDeposit.amount > maxYieldAmount) { - userDeposit.amount = maxYieldAmount; + function calculateInterest(uint256 userBalance, UserDeposit memory userDeposit) internal view returns (uint256) { + if (userBalance > maxYieldAmount) { + userBalance = maxYieldAmount; } uint256 endTime; @@ -336,48 +366,40 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { } uint256 yieldRate = getYieldRateForScore(userDeposit.user); - return (userDeposit.amount * yieldRate * timeElapsed) / (SECONDS_PER_YEAR * ONE_HUNDRED_PERCENT); + return (userBalance * yieldRate * timeElapsed) / (SECONDS_PER_YEAR * ONE_HUNDRED_PERCENT); } /// @dev Refreshes the interest on a user's deposit /// @param userDeposit The user's deposit function refreshInterest(UserDeposit storage userDeposit) internal { - if (userDeposit.amount == 0) return; - - uint256 interest = calculateInterest(userDeposit); - userDeposit.amount += interest; - userDeposit.lastInterestCalculation = block.timestamp; + // if (userDeposit.amount == 0) return; + // uint256 interest = calculateInterest(userDeposit); + // userDeposit.amount += interest; + // userDeposit.lastInterestCalculation = block.timestamp; } /// @dev Withdraws the user's balance, including any accrued interest /// @param user The address of the user to withdraw the balance of /// @param amount The amount of tokens to withdraw function _withdraw(address user, uint256 amount) internal { - UserDeposit storage userDeposit = getDeposit[user]; - if (userDeposit.amount <= 0) { - revert NoDepositFound(); - } - - refreshInterest(userDeposit); - require(userDeposit.amount >= amount, "Not enough balance"); - - uint256 contractBalance = token.balanceOf(address(this)); - uint256 fromContractAmount = amount < userDeposit.depositedAmount ? amount : userDeposit.depositedAmount; - uint256 fromYieldSourceAmount = amount - fromContractAmount; - - require(contractBalance >= fromContractAmount, "Contract insolvent"); - - userDeposit.amount -= amount; - userDeposit.depositedAmount -= fromContractAmount; - - emit Withdrawn(user, amount); - - if (fromContractAmount > 0) { - require(token.transfer(user, fromContractAmount), "Transfer failed"); - } - - if (fromYieldSourceAmount > 0) { - require(token.transferFrom(yieldSource, user, fromYieldSourceAmount), "Transfer failed"); - } + // UserDeposit storage userDeposit = getDeposit[user]; + // if (userDeposit.amount <= 0) { + // revert NoDepositFound(); + // } + // refreshInterest(userDeposit); + // require(userDeposit.amount >= amount, "Not enough balance"); + // uint256 contractBalance = token.balanceOf(address(this)); + // uint256 fromContractAmount = amount < userDeposit.depositedAmount ? amount : userDeposit.depositedAmount; + // uint256 fromYieldSourceAmount = amount - fromContractAmount; + // require(contractBalance >= fromContractAmount, "Contract insolvent"); + // userDeposit.amount -= amount; + // userDeposit.depositedAmount -= fromContractAmount; + // emit Withdrawn(user, amount); + // if (fromContractAmount > 0) { + // require(token.transfer(user, fromContractAmount), "Transfer failed"); + // } + // if (fromYieldSourceAmount > 0) { + // require(token.transferFrom(yieldSource, user, fromYieldSourceAmount), "Transfer failed"); + // } } } diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 7773b4e9..9ec29d75 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -6,6 +6,7 @@ import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { TalentProtocolToken, TalentVault, PassportRegistry, PassportBuilderScore } from "../../../typechain-types"; import { Artifacts } from "../../shared"; import { TalentVault as TalentVaultArtifact } from "../../shared/artifacts"; +import { talent } from "../../../typechain-types/contracts"; chai.use(solidity); @@ -32,18 +33,35 @@ describe("TalentVault", () => { passportRegistry.address, admin.address, ])) as PassportBuilderScore; + + const adminInitialDeposit = ethers.utils.parseEther("20000"); talentVault = (await deployContract(admin, Artifacts.TalentVault, [ talentToken.address, admin.address, ethers.utils.parseEther("10000"), passportBuilderScore.address, - ethers.utils.parseEther("10000"), + // adminInitialDeposit, ])) as TalentVault; + console.log("------------------------------------"); + console.log("Addresses:"); + console.log(`admin = ${admin.address}`); + console.log(`user1 = ${user1.address}`); + console.log(`user2 = ${user2.address}`); + console.log(`user3 = ${user3.address}`); + console.log(`talentToken = ${talentToken.address}`); + console.log(`talentVault = ${talentVault.address}`); + console.log("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); + // Approve TalentVault contract to spend tokens on behalf of the admin const totalAllowance = ethers.utils.parseUnits("600000000", 18); await talentToken.approve(talentVault.address, totalAllowance); await talentToken.unpause(); + + // just make sure that TV wallet has $TALENT as initial assets from admin initial deposit + await talentToken.approve(talentVault.address, adminInitialDeposit); + await talentVault.mint(adminInitialDeposit, admin.address); + // await talentToken.renounceOwnership(); }); @@ -152,6 +170,25 @@ describe("TalentVault", () => { expect(returnedValue).to.equal(balanceOfTalentVaultInTalent); }); }); + + describe("Transferability", async () => { + describe("#transfer", async () => { + it("reverts because TalentVault is not transferable", async () => { + await expect(talentVault.transfer(user1.address, 10n)).to.be.revertedWith("TalentVaultNonTransferable"); + }); + }); + + describe("#transferFrom", async () => { + it("reverts because TalentVault is not transferable", async () => { + await talentVault.approve(admin.address, 10n); + // fire + await expect(talentVault.transferFrom(admin.address, user2.address, 10n)).to.be.revertedWith( + "TalentVaultNonTransferable" + ); + }); + }); + }); + describe("#setMaxDeposit", async () => { context("when called by the owner", async () => { it("sets the maximum deposit for the receiver", async () => { @@ -172,8 +209,36 @@ describe("TalentVault", () => { }); }); + describe("#removeMaxDepositLimit", async () => { + context("when called by the owner", async () => { + it("removes the maximum deposit for the receiver", async () => { + await talentVault.removeMaxDepositLimit(user1.address); + + const deposit = await talentVault.maxDeposit(user1.address); + + expect(deposit).to.equal(ethers.constants.MaxUint256); + }); + }); + + context("when called by a non-owner", async () => { + it("reverts", async () => { + await expect(talentVault.connect(user1).removeMaxDepositLimit(user2.address)).to.revertedWith( + "OwnableUnauthorizedAccount" + ); + }); + }); + }); + describe("#maxDeposit", async () => { - context("when recipient has positive maximum deposit limit", async () => { + context("when recipient does not have a deposit limit", async () => { + it("returns the maximum uint256", async () => { + const maxDeposit = await talentVault.maxDeposit(user1.address); + + expect(maxDeposit).to.equal(ethers.constants.MaxUint256); + }); + }); + + context("when recipient has a deposit limit", async () => { it("returns it", async () => { await talentVault.setMaxDeposit(user1.address, 5n); @@ -184,135 +249,392 @@ describe("TalentVault", () => { }); }); - describe("Transferability", async () => { - describe("#transfer", async () => { - it("reverts because TalentVault is not transferable", async () => { - await expect(talentVault.transfer(user1.address, 10n)).to.be.revertedWith("TalentVaultNonTransferable"); - }); + describe("#convertToShares", async () => { + it("Should convert $TALENT to $TALENTVAULT with 1-to-1 ratio", async () => { + const amountOfTalent = 10_000n; + const amountOfTalentVault = await talentVault.convertToShares(amountOfTalent); + expect(amountOfTalentVault).to.equal(amountOfTalent); }); + }); - describe("#transferFrom", async () => { - it("reverts because TalentVault is not transferable", async () => { - await talentVault.approve(admin.address, 10n); - // fire - await expect(talentVault.transferFrom(admin.address, user2.address, 10n)).to.be.revertedWith( - "TalentVaultNonTransferable" - ); - }); + describe("#convertToAssets", async () => { + it("Should convert $TALENTVAULT to $TALENT with 1-to-1 ratio", async () => { + const amountOfTalentVault = 10_000n; + const amountOfTalent = await talentVault.convertToAssets(amountOfTalentVault); + expect(amountOfTalent).to.equal(amountOfTalentVault); }); }); - describe("Deposits", () => { - it("Should allow users to deposit tokens", async () => { - const depositAmount = 10_000n; - - await talentToken.transfer(user1.address, depositAmount); // so that it has enough balance - const user1BalanceBefore = await talentToken.balanceOf(user1.address); + describe("#previewDeposit", async () => { + it("Should return $TALENTVAULT equal to the number of $TALENT given", async () => { + const amountOfTalent = 10_000n; + const amountOfTalentVault = await talentVault.previewDeposit(amountOfTalent); + expect(amountOfTalentVault).to.equal(amountOfTalent); + }); + }); - await talentToken.connect(user1).approve(talentVault.address, depositAmount); + describe("#deposit", async () => { + it("Should mint $TALENTVAULT to the given receiver, equally increase the TalentVault $TALENT balance and equally decrease the $TALENT balance of receiver", async () => { + const depositAmountInTalent = 10_000n; + const equivalentDepositAmountInTalentVault = depositAmountInTalent; + await talentToken.connect(user1).approve(talentVault.address, depositAmountInTalent); + await talentToken.transfer(user1.address, depositAmountInTalent); // so that it has enough balance + const user1BalanceBefore = await talentToken.balanceOf(user1.address); + const user1BalanceInTalentVaultBefore = await talentVault.balanceOf(user1.address); const vaultBalanceBefore = await talentToken.balanceOf(talentVault.address); + const userDepositBefore = await talentVault.getDeposit(user1.address); + const depositedAmountBefore = userDepositBefore.depositedAmount; - const user1BalanceInTalentVaultBefore = await talentVault.balanceOf(user1.address); - - // fire - await expect(talentVault.connect(user1).deposit(depositAmount)) - .to.emit(talentVault, "Deposited") - .withArgs(user1.address, depositAmount); + // fire (admin deposits to itself) + await expect(talentVault.connect(user1).deposit(depositAmountInTalent, user1.address)) + .to.emit(talentVault, "Deposit") + .withArgs(user1.address, user1.address, depositAmountInTalent, equivalentDepositAmountInTalentVault); // vault balance in TALENT is increased const vaultBalanceAfter = await talentToken.balanceOf(talentVault.address); - const expectedVaultBalanceAfter = vaultBalanceBefore.toBigInt() + depositAmount; + const expectedVaultBalanceAfter = vaultBalanceBefore.toBigInt() + depositAmountInTalent; expect(vaultBalanceAfter).to.equal(expectedVaultBalanceAfter); // user1 balance in TALENT decreases const user1BalanceAfter = await talentToken.balanceOf(user1.address); - expect(user1BalanceAfter).to.equal(user1BalanceBefore.toBigInt() - depositAmount); + expect(user1BalanceAfter).to.equal(user1BalanceBefore.toBigInt() - depositAmountInTalent); - // user1 balance in TalentVault increases + // user1 balance in TalentVault increases (mint result) const user1BalanceInTalentVaultAfter = await talentVault.balanceOf(user1.address); - expect(user1BalanceInTalentVaultAfter).to.equal(user1BalanceInTalentVaultBefore.toBigInt() + depositAmount); + expect(user1BalanceInTalentVaultAfter).to.equal( + user1BalanceInTalentVaultBefore.toBigInt() + equivalentDepositAmountInTalentVault + ); + + // user1 depositedAmount is increased + const userDeposit = await talentVault.getDeposit(user1.address); + const depositedAmountAfter = userDeposit.depositedAmount; + expect(depositedAmountAfter).to.equal(depositedAmountBefore.toBigInt() + equivalentDepositAmountInTalentVault); }); - describe("#depositForAddress", async () => { - it("Should deposit the amount to the address given", async () => { - const depositAmount = 100_000n; - await talentToken.transfer(user1.address, depositAmount); // so that sender has enough balance - const user1BalanceBefore = await talentToken.balanceOf(user1.address); + it("Should revert if $TALENT deposited is 0", async () => { + await expect(talentVault.connect(user1).deposit(0n, user1.address)).to.be.revertedWith("InvalidDepositAmount"); + }); + }); - await talentToken.connect(user1).approve(talentVault.address, depositAmount); // so that sender has approved vault + describe("#setMaxMint", async () => { + context("when called by the owner", async () => { + it("sets the maximum mint for the receiver", async () => { + await talentVault.setMaxMint(user1.address, 10n); - const vaultBalanceBefore = await talentToken.balanceOf(talentVault.address); + const mint = await talentVault.maxMint(user1.address); - const user2DepositBefore = await talentVault.getDeposit(user2.address); + expect(mint).to.equal(10n); + }); + }); - // fire - await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)) - .to.emit(talentVault, "Deposited") - .withArgs(user2.address, depositAmount); - - // user1 talent balance is decreased - const user1BalanceAfter = await talentVault.balanceOf(user1.address); - const expectedUser1BalanceAfter = user1BalanceBefore.sub(depositAmount); - expect(user1BalanceAfter).to.equal(expectedUser1BalanceAfter); - - // vault balance is increased - const vaultBalanceAfter = await talentToken.balanceOf(talentVault.address); - const expectedVaultBalanceAfter = vaultBalanceBefore.toBigInt() + depositAmount; - expect(vaultBalanceAfter).to.equal(expectedVaultBalanceAfter); - - // deposit for user2 is updated on storage - const user2DepositAfter = await talentVault.getDeposit(user2.address); - expect(user2DepositAfter.user).to.equal(user2.address); - expect(user2DepositAfter.depositedAmount).to.equal( - user2DepositBefore.depositedAmount.toBigInt() + depositAmount + context("when called by a non-owner", async () => { + it("reverts", async () => { + await expect(talentVault.connect(user1).setMaxMint(user2.address, 10n)).to.revertedWith( + "OwnableUnauthorizedAccount" ); - expect(user2DepositAfter.amount).to.equal(user2DepositBefore.amount.toBigInt() + depositAmount); }); + }); + }); + + describe("#removeMaxMintLimit", async () => { + context("when called by the owner", async () => { + it("removes the maximum mint for the receiver", async () => { + await talentVault.removeMaxMintLimit(user1.address); + + const mint = await talentVault.maxMint(user1.address); + + expect(mint).to.equal(ethers.constants.MaxUint256); + }); + }); - it("Should not allow deposits of zero tokens", async () => { - await expect(talentVault.connect(user1).depositForAddress(ethers.constants.AddressZero, 0n)).to.be.revertedWith( - "InvalidDepositAmount()" + context("when called by a non-owner", async () => { + it("reverts", async () => { + await expect(talentVault.connect(user1).removeMaxMintLimit(user2.address)).to.revertedWith( + "OwnableUnauthorizedAccount" ); }); + }); + }); - it("Should not allow deposit of amount that the sender does not have", async () => { - const balanceOfUser1 = 100_000n; + describe("#maxMint", async () => { + context("when recipient does not have a mint limit", async () => { + it("returns the maximum uint256", async () => { + const maxMint = await talentVault.maxMint(user1.address); - await talentToken.transfer(user1.address, balanceOfUser1); + expect(maxMint).to.equal(ethers.constants.MaxUint256); + }); + }); + + context("when recipient has a mint limit", async () => { + it("returns it", async () => { + await talentVault.setMaxMint(user1.address, 5n); - const depositAmount = 100_001n; + const maxMint = await talentVault.maxMint(user1.address); - await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)).to.be.revertedWith( - "InsufficientBalance()" - ); + expect(maxMint).to.equal(5n); }); + }); + }); - it("Should not allow deposit of amount bigger than the allowed by the sender to be spent by the talent contract", async () => { - const depositAmount = 100_000n; + describe("#previewMint", async () => { + it("Should return $TALENT equal to the number of $TALENTVAULT given", async () => { + const amountOfTalentVault = 10_000n; + const amountOfTalent = await talentVault.previewMint(amountOfTalentVault); + expect(amountOfTalent).to.equal(amountOfTalentVault); + }); + }); - await talentToken.transfer(user1.address, depositAmount); // so that user1 has enough balance + describe("#mint", async () => { + it("Should mint $TALENTVAULT to the given receiver, equally increase the TalentVault $TALENT balance and equally decrease the $TALENT balance of receiver", async () => { + const depositAmountInTalentVault = 10_000n; + const equivalentDepositAmountInTalent = depositAmountInTalentVault; - const approvedAmount = depositAmount - 1n; + await talentToken.connect(user1).approve(talentVault.address, depositAmountInTalentVault); + await talentToken.transfer(user1.address, depositAmountInTalentVault); // so that it has enough balance + const user1BalanceBefore = await talentToken.balanceOf(user1.address); + const user1BalanceInTalentVaultBefore = await talentVault.balanceOf(user1.address); + const vaultBalanceBefore = await talentToken.balanceOf(talentVault.address); + const userDepositBefore = await talentVault.getDeposit(user1.address); + const depositedAmountBefore = userDepositBefore.depositedAmount; - await talentToken.connect(user1).approve(talentVault.address, approvedAmount); + // fire (admin deposits to itself) + await expect(talentVault.connect(user1).mint(depositAmountInTalentVault, user1.address)) + .to.emit(talentVault, "Deposit") + .withArgs(user1.address, user1.address, equivalentDepositAmountInTalent, depositAmountInTalentVault); - // fire + // vault balance in TALENT is increased + const vaultBalanceAfter = await talentToken.balanceOf(talentVault.address); + const expectedVaultBalanceAfter = vaultBalanceBefore.toBigInt() + depositAmountInTalentVault; + expect(vaultBalanceAfter).to.equal(expectedVaultBalanceAfter); - await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)).to.be.revertedWith( - "InsufficientAllowance()" - ); - }); + // user1 balance in TALENT decreases + const user1BalanceAfter = await talentToken.balanceOf(user1.address); + expect(user1BalanceAfter).to.equal(user1BalanceBefore.toBigInt() - depositAmountInTalentVault); + + // user1 balance in TalentVault increases (mint result) + const user1BalanceInTalentVaultAfter = await talentVault.balanceOf(user1.address); + expect(user1BalanceInTalentVaultAfter).to.equal( + user1BalanceInTalentVaultBefore.toBigInt() + equivalentDepositAmountInTalent + ); - it("Should allow deposit of amount equal to the allowed by the sender to be spent by the talent contract", async () => { - const depositAmount = ethers.utils.parseEther("1000"); + // user1 depositedAmount is increased + const userDeposit = await talentVault.getDeposit(user1.address); + const depositedAmountAfter = userDeposit.depositedAmount; + expect(depositedAmountAfter).to.equal(depositedAmountBefore.toBigInt() + equivalentDepositAmountInTalent); + }); - await talentToken.connect(user1).approve(talentVault.address, depositAmount); + it("Should revert if $TALENT deposited is 0", async () => { + await expect(talentVault.connect(user1).deposit(0n, user1.address)).to.be.revertedWith("InvalidDepositAmount"); + }); + }); - await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)).to.be.revertedWith( - "InsufficientBalance()" - ); - }); + describe("#maxWithdraw", async () => { + it("returns the balance of $TALENTVAULT of the given owner", async () => { + // just setting up some non-zero values to make test more solid + const depositAmount = 10_000n; + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount, user1.address); + const balance = await talentVault.balanceOf(user1.address); + + // fire + const maxWithdraw = await talentVault.maxWithdraw(user1.address); + + expect(maxWithdraw).to.equal(balance); + }); + }); + + describe("#previewWithdraw", async () => { + it("Should return $TALENTVAULT equal to the number of $TALENT given", async () => { + const amountOfTalent = 10_000n; + const amountOfTalentVault = await talentVault.previewWithdraw(amountOfTalent); + expect(amountOfTalentVault).to.equal(amountOfTalent); + }); + }); + + describe("#withDraw", async () => { + it("burns $TALENTVAULT from owner, increases $TALENT balance of receiver, decreases $TALENT balance of TalentVault", async () => { + const depositTalent = 10_000n; + + await talentToken.transfer(user1.address, depositTalent); + await talentToken.connect(user1).approve(talentVault.address, depositTalent); + let trx = await talentVault.connect(user1).deposit(depositTalent, user1.address); + await trx.wait(); + + const user1TalentVaultBalanceBefore = await talentVault.balanceOf(user1.address); + const user1TalentBalanceBefore = await talentToken.balanceOf(user1.address); + const talentVaultTalentBalanceBefore = await talentToken.balanceOf(talentVault.address); + + // fire + trx = await talentVault.connect(user1).withdraw(depositTalent, user1.address, user1.address); + const receipt = await trx.wait(); + + const withdrawEvent = receipt.events.find((event) => event.event === "Withdraw"); + + const talentVaultWithDrawn = withdrawEvent.args[4]; + + expect(talentVaultWithDrawn).to.equal(depositTalent); + + // user1 $TALENTVAULT balance decreases + const user1TalentVaultBalanceAfter = await talentVault.balanceOf(user1.address); + expect(user1TalentVaultBalanceAfter).to.equal(user1TalentVaultBalanceBefore.toBigInt() - depositTalent); + + // user1 $TALENT balance increases + const user1TalentBalanceAfter = await talentToken.balanceOf(user1.address); + expect(user1TalentBalanceAfter).to.equal(user1TalentBalanceBefore.toBigInt() + depositTalent); + + // TalentVault $TALENT balance decreases + const talentVaultTalentBalanceAfter = await talentToken.balanceOf(talentVault.address); + expect(talentVaultTalentBalanceAfter).to.equal(talentVaultTalentBalanceBefore.toBigInt() - depositTalent); + }); + }); + + describe("#maxRedeem", async () => { + it("returns the balance of $TALENTVAULT of the given owner", async () => { + // just setting up some non-zero values to make test more solid + const depositAmount = 10_000n; + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount, user1.address); + const balance = await talentVault.balanceOf(user1.address); + + // fire + const maxRedeem = await talentVault.maxRedeem(user1.address); + + expect(maxRedeem).to.equal(balance); + }); + }); + + describe("#previewRedeem", async () => { + it("Should return $TALENT equal to the number of $TALENTVAULT given", async () => { + const amountOfTalentVault = 10_000n; + const amountOfTalent = await talentVault.previewRedeem(amountOfTalentVault); + expect(amountOfTalent).to.equal(amountOfTalentVault); + }); + }); + + describe("#redeem", async () => { + it("burns $TALENTVAULT from owner, increases $TALENT balance of receiver, decreases $TALENT balance of TalentVault", async () => { + const depositTalent = 10_000n; + const equivalentDepositTalentVault = depositTalent; + + await talentToken.transfer(user1.address, depositTalent); + await talentToken.connect(user1).approve(talentVault.address, depositTalent); + let trx = await talentVault.connect(user1).deposit(depositTalent, user1.address); + await trx.wait(); + + const user1TalentVaultBalanceBefore = await talentVault.balanceOf(user1.address); + const user1TalentBalanceBefore = await talentToken.balanceOf(user1.address); + const talentVaultTalentBalanceBefore = await talentToken.balanceOf(talentVault.address); + + // fire + trx = await talentVault.connect(user1).redeem(equivalentDepositTalentVault, user1.address, user1.address); + const receipt = await trx.wait(); + + const withdrawEvent = receipt.events.find((event) => event.event === "Withdraw"); + + const talentWithDrawn = withdrawEvent.args[4]; + + expect(talentWithDrawn).to.equal(equivalentDepositTalentVault); + + // user1 $TALENTVAULT balance decreases + const user1TalentVaultBalanceAfter = await talentVault.balanceOf(user1.address); + expect(user1TalentVaultBalanceAfter).to.equal(user1TalentVaultBalanceBefore.toBigInt() - depositTalent); + + // user1 $TALENT balance increases + const user1TalentBalanceAfter = await talentToken.balanceOf(user1.address); + expect(user1TalentBalanceAfter).to.equal(user1TalentBalanceBefore.toBigInt() + depositTalent); + + // TalentVault $TALENT balance decreases + const talentVaultTalentBalanceAfter = await talentToken.balanceOf(talentVault.address); + expect(talentVaultTalentBalanceAfter).to.equal(talentVaultTalentBalanceBefore.toBigInt() - depositTalent); + }); + }); + + describe("#depositForAddress", async () => { + it("Should deposit the amount to the address given", async () => { + const depositAmount = 100_000n; + await talentToken.transfer(user1.address, depositAmount); // so that sender has enough balance + const user1BalanceBefore = await talentToken.balanceOf(user1.address); + + await talentToken.connect(user1).approve(talentVault.address, depositAmount); // so that sender has approved vault + + const vaultBalanceBefore = await talentToken.balanceOf(talentVault.address); + + const user2DepositBefore = await talentVault.getDeposit(user2.address); + + const user2TalentVaultBalanceBefore = await talentVault.balanceOf(user2.address); + + // fire + await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)) + .to.emit(talentVault, "Deposit") + .withArgs(user1.address, user2.address, depositAmount, depositAmount); + + // user1 $TALENT balance is decreased + const user1BalanceAfter = await talentToken.balanceOf(user1.address); + const expectedUser1BalanceAfter = user1BalanceBefore.sub(depositAmount); + expect(user1BalanceAfter).to.equal(expectedUser1BalanceAfter); + + // vault $TALENT balance is increased + const vaultBalanceAfter = await talentToken.balanceOf(talentVault.address); + const expectedVaultBalanceAfter = vaultBalanceBefore.toBigInt() + depositAmount; + expect(vaultBalanceAfter).to.equal(expectedVaultBalanceAfter); + + // deposit for user2 is updated on storage + const user2DepositAfter = await talentVault.getDeposit(user2.address); + expect(user2DepositAfter.user).to.equal(user2.address); + expect(user2DepositAfter.depositedAmount).to.equal(user2DepositBefore.depositedAmount.toBigInt() + depositAmount); + + // user2 $TALENTVAULT balance is increased + const user2TalentVaultBalanceAfter = await talentVault.balanceOf(user2.address); + expect(user2TalentVaultBalanceAfter).to.equal(user2TalentVaultBalanceBefore.toBigInt() + depositAmount); + }); + + it("Should not allow deposits of zero tokens", async () => { + await expect(talentVault.connect(user1).depositForAddress(ethers.constants.AddressZero, 0n)).to.be.revertedWith( + "InvalidDepositAmount()" + ); + }); + + it("Should not allow deposit of amount that the sender does not have", async () => { + const balanceOfUser1 = 100_000n; + + await talentToken.transfer(user1.address, balanceOfUser1); + + const depositAmount = 100_001n; + + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + + await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)).to.be.revertedWith( + "ERC20InsufficientBalance" + ); + }); + + it("Should not allow deposit of amount bigger than the allowed by the sender to be spent by the talent contract", async () => { + const depositAmount = 100_000n; + + await talentToken.transfer(user1.address, depositAmount); // so that user1 has enough balance + + const approvedAmount = depositAmount - 1n; + + await talentToken.connect(user1).approve(talentVault.address, approvedAmount); + + // fire + + await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)).to.be.revertedWith( + "ERC20InsufficientAllowance" + ); + }); + + it("Should allow deposit of amount equal to the allowed by the sender to be spent by the talent contract", async () => { + const depositAmount = ethers.utils.parseEther("1000"); + + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + + await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)).to.be.revertedWith( + "ERC20InsufficientBalance" + ); }); }); From fe552bd6987873344bff5fc01312109972900464 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Wed, 30 Oct 2024 13:26:12 +0200 Subject: [PATCH 30/74] Some comments --- test/contracts/talent/TalentVault.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 9ec29d75..38df8dcd 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -135,6 +135,8 @@ describe("TalentVault", () => { it("can be unpaused only by the owner"); }); + // TODO: StopYieldingInterest + describe("#name", async () => { it("is 'TalentProtocolVaultToken' reflects the underlying token name, i.e. of 'TalentProtocolToken'", async () => { const name = await talentVault.name(); @@ -274,7 +276,7 @@ describe("TalentVault", () => { }); describe("#deposit", async () => { - it("Should mint $TALENTVAULT to the given receiver, equally increase the TalentVault $TALENT balance and equally decrease the $TALENT balance of receiver", async () => { + it("Should mint $TALENTVAULT to the given receiver, equally increase the TalentVault $TALENT balance and equally decreases the $TALENT balance of receiver", async () => { const depositAmountInTalent = 10_000n; const equivalentDepositAmountInTalentVault = depositAmountInTalent; From 35ce9b3b3c0f36e6e6183cb7e570e37cb4d1eaf9 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Wed, 30 Oct 2024 18:06:26 +0200 Subject: [PATCH 31/74] Add hardhat-storage-layout --- package.json | 1 + yarn.lock | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/package.json b/package.json index 979aa8ce..a2eb1480 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "dotenv": "^16.0.3", "eslint": "^8.19.0", "ethereum-waffle": "3.4.0", + "hardhat-storage-layout": "^0.1.7", "prettier": "^2.4.1", "prettier-plugin-solidity": "^1.0.0-beta.18", "solc": "^0.8.25" diff --git a/yarn.lock b/yarn.lock index 4f6c4751..39eff526 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3370,6 +3370,13 @@ concat-stream@^1.5.1, concat-stream@^1.6.0, concat-stream@^1.6.2: readable-stream "^2.2.2" typedarray "^0.0.6" +console-table-printer@^2.9.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/console-table-printer/-/console-table-printer-2.12.1.tgz#4a9646537a246a6d8de57075d4fae1e08abae267" + integrity sha512-wKGOQRRvdnd89pCeH96e2Fn4wkbenSP6LMHfjfyNLMbGuHEFbMqQNuxXqd0oXG9caIOQ1FTvc5Uijp9/4jujnQ== + dependencies: + simple-wcswidth "^1.0.1" + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -5537,6 +5544,13 @@ hardhat-gas-reporter@^1.0.9: eth-gas-reporter "^0.2.25" sha1 "^1.1.1" +hardhat-storage-layout@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/hardhat-storage-layout/-/hardhat-storage-layout-0.1.7.tgz#ad8a5afd8593ee51031eb1dd9476b4a2ed981785" + integrity sha512-q723g2iQnJpRdMC6Y8fbh/stG6MLHKNxa5jq/ohjtD5znOlOzQ6ojYuInY8V4o4WcPyG3ty4hzHYunLf66/1+A== + dependencies: + console-table-printer "^2.9.0" + hardhat@2.22.4, hardhat@^2.22.4: version "2.22.4" resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.22.4.tgz#766227b6cefca5dbf4fd15ab5b5a68138fa13baf" @@ -8800,6 +8814,11 @@ simple-get@^2.7.0: once "^1.3.1" simple-concat "^1.0.0" +simple-wcswidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz#8ab18ac0ae342f9d9b629604e54d2aa1ecb018b2" + integrity sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg== + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" From 150af90cbaa139c9aeaa2c8e8eb50a52d7931ab5 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Wed, 30 Oct 2024 19:29:37 +0200 Subject: [PATCH 32/74] All tested except the refresh part --- contracts/talent/TalentVault.sol | 102 +++------ hardhat.config.ts | 7 +- test/contracts/talent/TalentVault.ts | 309 ++++++++++++++------------- 3 files changed, 195 insertions(+), 223 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 632ad216..74d2caa1 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -25,19 +25,6 @@ error TransferFailed(); contract TalentVault is ERC4626, Ownable, ReentrancyGuard { using SafeERC20 for IERC20; - // This is not needed, since ERC4626 already emits - // event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares) - - // /// @notice Emitted when a user deposits tokens - // /// @param user The address of the user who deposited tokens - // /// @param amount The amount of tokens deposited - // event Deposited(address indexed user, uint256 amount); - - // /// @notice Emitted when a user withdraws tokens - // /// @param user The address of the user who withdrew tokens - // /// @param amount The amount of tokens withdrawn - // event Withdrawn(address indexed user, uint256 amount); - /// @notice Emitted when the yield rate is updated /// @param yieldRate The new yield rate event YieldRateUpdated(uint256 yieldRate); @@ -53,10 +40,10 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice Represents a user's deposit /// @param depositedAmount The amount of tokens that were deposited, excluding interest /// @param lastInterestCalculation The timestamp of the last interest calculation for this deposit - struct UserDeposit { + struct UserBalanceMeta { uint256 depositedAmount; uint256 lastInterestCalculation; - address user; + // address user; } /// @notice The number of seconds in a year @@ -65,6 +52,8 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice The maximum yield rate that can be set, represented as a percentage. uint256 public constant ONE_HUNDRED_PERCENT = 100_00; + uint256 public constant SECONDS_PER_YEAR_x_ONE_HUNDRED_PERCENT = SECONDS_PER_YEAR * ONE_HUNDRED_PERCENT; + /// @notice The token that will be deposited into the contract IERC20 public immutable token; @@ -96,7 +85,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { PassportBuilderScore public passportBuilderScore; /// @notice A mapping of user addresses to their deposits - mapping(address => UserDeposit) public getDeposit; + mapping(address => UserBalanceMeta) public userBalanceMeta; mapping(address => bool) private maxDepositLimitFlags; mapping(address => uint256) private maxDeposits; @@ -135,12 +124,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { passportBuilderScore = _passportBuilderScore; } - // /// @notice UserDeposit tokens into the contract, which will start accruing interest. - // /// @param amount The amount of tokens to deposit - // function deposit(uint256 amount) public { - // depositForAddress(msg.sender, amount); - // } - function setMaxDeposit(address receiver, uint256 assets) public onlyOwner { maxDeposits[receiver] = assets; maxDepositLimitFlags[receiver] = true; @@ -167,8 +150,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { } } - // @dev We consider +shares+ and +assets+ to have 1-to-1 equivalence - // Hence, deposits and mints are treated equally function maxMint(address receiver) public view virtual override returns (uint256) { return maxDeposit(receiver); } @@ -178,26 +159,16 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { revert InvalidDepositAmount(); } - UserDeposit storage userDeposit = getDeposit[receiver]; + uint256 shares = super.deposit(assets, receiver); + + UserBalanceMeta storage balanceMeta = userBalanceMeta[receiver]; // This we don't need it only for reporting purposes. // Note that the +assets+ is $TALENT that is moved from // +receiver+ wallet to +address(this)+ wallet. // But this one here, it gives us an easy way to find out - // how much $TALENT a +receiver+ has deposited. - userDeposit.depositedAmount += assets; - - userDeposit.user = receiver; // Why do we need this? - - uint256 balanceOfReceiverBeforeDeposit = balanceOf(receiver); - - uint256 shares = super.deposit(assets, receiver); - - if (balanceOfReceiverBeforeDeposit > 0) { - uint256 interest = calculateInterest(balanceOfReceiverBeforeDeposit, userDeposit); - userDeposit.lastInterestCalculation = block.timestamp; - _deposit(yieldSource, receiver, interest, interest); - } + // how much $TALENT a +receiveruserBalanceMetadeposited. + balanceMeta.depositedAmount += assets; return shares; } @@ -216,12 +187,11 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice Calculate any accrued interest. /// @param account The address of the user to refresh function refreshForAddress(address account) public { - UserDeposit storage userDeposit = getDeposit[account]; if (balanceOf(account) <= 0) { revert NoDepositFound(); } - refreshInterest(userDeposit); + yieldInterest(account); } /// @notice Calculate any accrued interest. @@ -229,19 +199,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { refreshForAddress(msg.sender); } - /// @notice Returns the balance of the user, including any accrued interest. - /// @param user The address of the user to check the balance of - function balanceOf(address user) public view virtual override(ERC20, IERC20) returns (uint256) { - return super.balanceOf(user); - - // UserDeposit storage userDeposit = getDeposit[user]; - // if (balanceOf(user) == 0) return 0; - - // uint256 interest = calculateInterest(userDeposit); - - // return userDeposit.amount + interest; - } - /// @notice Withdraws the requested amount from the user's balance. // function withdraw(uint256 amount) external nonReentrant { // _withdraw(msg.sender, amount); @@ -341,10 +298,11 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { revert TalentVaultNonTransferable(); } - /// @dev Calculates the interest accrued on the deposit - /// @param userDeposit The user's deposit - /// @return The amount of interest accrued - function calculateInterest(uint256 userBalance, UserDeposit memory userDeposit) internal view returns (uint256) { + // ---------- INTERNAL -------------------------------------- + + function calculateInterest(address user) internal returns (uint256) { + uint256 userBalance = balanceOf(user); + if (userBalance > maxYieldAmount) { userBalance = maxYieldAmount; } @@ -356,26 +314,30 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { endTime = block.timestamp; } + UserBalanceMeta storage balanceMeta = userBalanceMeta[user]; + uint256 timeElapsed; + if (block.timestamp > endTime) { - timeElapsed = endTime > userDeposit.lastInterestCalculation - ? endTime - userDeposit.lastInterestCalculation + timeElapsed = endTime > balanceMeta.lastInterestCalculation + ? endTime - balanceMeta.lastInterestCalculation : 0; } else { - timeElapsed = block.timestamp - userDeposit.lastInterestCalculation; + timeElapsed = block.timestamp - balanceMeta.lastInterestCalculation; } - uint256 yieldRate = getYieldRateForScore(userDeposit.user); - return (userBalance * yieldRate * timeElapsed) / (SECONDS_PER_YEAR * ONE_HUNDRED_PERCENT); + uint256 yieldRate = getYieldRateForScore(user); + + balanceMeta.lastInterestCalculation = block.timestamp; + + return (userBalance * yieldRate * timeElapsed) / (SECONDS_PER_YEAR_x_ONE_HUNDRED_PERCENT); } - /// @dev Refreshes the interest on a user's deposit - /// @param userDeposit The user's deposit - function refreshInterest(UserDeposit storage userDeposit) internal { - // if (userDeposit.amount == 0) return; - // uint256 interest = calculateInterest(userDeposit); - // userDeposit.amount += interest; - // userDeposit.lastInterestCalculation = block.timestamp; + /// @dev Refreshes the balance of an address + function yieldInterest(address user) internal { + uint256 interest = calculateInterest(user); + + _deposit(yieldSource, user, interest, interest); } /// @dev Withdraws the user's balance, including any accrued interest diff --git a/hardhat.config.ts b/hardhat.config.ts index b19f77a8..f3969064 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,5 +1,5 @@ import { task } from "hardhat/config"; - +import "hardhat-storage-layout"; import "@typechain/hardhat"; import "@nomiclabs/hardhat-ethers"; import "@nomicfoundation/hardhat-viem"; @@ -32,6 +32,11 @@ const config: HardhatUserConfig = { enabled: true, runs: 1000, }, + outputSelection: { + "*": { + "*": ["storageLayout"], + }, + }, }, }, networks: { diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 38df8dcd..bba9882a 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -71,11 +71,6 @@ describe("TalentVault", () => { expect(await talentVault.owner()).to.equal(admin.address); }); - it("Should set some initial balance for the owner", async () => { - const ownerBalance = await talentVault.balanceOf(admin.address); - expect(ownerBalance).to.equal(ethers.utils.parseEther("10000")); - }); - it("Should set the correct initial values", async () => { expect(await talentVault.yieldRateBase()).to.equal(10_00); expect(await talentVault.yieldRateProficient()).to.equal(15_00); @@ -164,6 +159,7 @@ describe("TalentVault", () => { describe("#totalAssets", async () => { it("returns the number of $TALENT that TalentVault Contract has as balance", async () => { + await talentToken.approve(talentVault.address, 10n); await talentVault.deposit(10n, user1.address); const returnedValue = await talentVault.totalAssets(); @@ -285,8 +281,8 @@ describe("TalentVault", () => { const user1BalanceBefore = await talentToken.balanceOf(user1.address); const user1BalanceInTalentVaultBefore = await talentVault.balanceOf(user1.address); const vaultBalanceBefore = await talentToken.balanceOf(talentVault.address); - const userDepositBefore = await talentVault.getDeposit(user1.address); - const depositedAmountBefore = userDepositBefore.depositedAmount; + const userBalanceMetaBefore = await talentVault.userBalanceMeta(user1.address); + const depositedAmountBefore = userBalanceMetaBefore.depositedAmount; // fire (admin deposits to itself) await expect(talentVault.connect(user1).deposit(depositAmountInTalent, user1.address)) @@ -309,8 +305,8 @@ describe("TalentVault", () => { ); // user1 depositedAmount is increased - const userDeposit = await talentVault.getDeposit(user1.address); - const depositedAmountAfter = userDeposit.depositedAmount; + const userBalanceMeta = await talentVault.userBalanceMeta(user1.address); + const depositedAmountAfter = userBalanceMeta.depositedAmount; expect(depositedAmountAfter).to.equal(depositedAmountBefore.toBigInt() + equivalentDepositAmountInTalentVault); }); @@ -397,8 +393,8 @@ describe("TalentVault", () => { const user1BalanceBefore = await talentToken.balanceOf(user1.address); const user1BalanceInTalentVaultBefore = await talentVault.balanceOf(user1.address); const vaultBalanceBefore = await talentToken.balanceOf(talentVault.address); - const userDepositBefore = await talentVault.getDeposit(user1.address); - const depositedAmountBefore = userDepositBefore.depositedAmount; + const userBalanceMetaBefore = await talentVault.userBalanceMeta(user1.address); + const depositedAmountBefore = userBalanceMetaBefore.depositedAmount; // fire (admin deposits to itself) await expect(talentVault.connect(user1).mint(depositAmountInTalentVault, user1.address)) @@ -421,8 +417,8 @@ describe("TalentVault", () => { ); // user1 depositedAmount is increased - const userDeposit = await talentVault.getDeposit(user1.address); - const depositedAmountAfter = userDeposit.depositedAmount; + const userBalanceMeta = await talentVault.userBalanceMeta(user1.address); + const depositedAmountAfter = userBalanceMeta.depositedAmount; expect(depositedAmountAfter).to.equal(depositedAmountBefore.toBigInt() + equivalentDepositAmountInTalent); }); @@ -564,7 +560,7 @@ describe("TalentVault", () => { const vaultBalanceBefore = await talentToken.balanceOf(talentVault.address); - const user2DepositBefore = await talentVault.getDeposit(user2.address); + const user2BalanceMetaBefore = await talentVault.userBalanceMeta(user2.address); const user2TalentVaultBalanceBefore = await talentVault.balanceOf(user2.address); @@ -584,9 +580,10 @@ describe("TalentVault", () => { expect(vaultBalanceAfter).to.equal(expectedVaultBalanceAfter); // deposit for user2 is updated on storage - const user2DepositAfter = await talentVault.getDeposit(user2.address); - expect(user2DepositAfter.user).to.equal(user2.address); - expect(user2DepositAfter.depositedAmount).to.equal(user2DepositBefore.depositedAmount.toBigInt() + depositAmount); + const user2BalanceMetaAfter = await talentVault.userBalanceMeta(user2.address); + expect(user2BalanceMetaAfter.depositedAmount).to.equal( + user2BalanceMetaBefore.depositedAmount.toBigInt() + depositAmount + ); // user2 $TALENTVAULT balance is increased const user2TalentVaultBalanceAfter = await talentVault.balanceOf(user2.address); @@ -640,142 +637,150 @@ describe("TalentVault", () => { }); }); - describe("Withdrawals", () => { - beforeEach(async () => { - const depositAmount = ethers.utils.parseEther("1000"); - await talentToken.transfer(user1.address, depositAmount); - await talentToken.connect(user1).approve(talentVault.address, depositAmount); - await talentVault.connect(user1).deposit(depositAmount); - }); - - it("Should allow users to withdraw tokens", async () => { - const withdrawAmount = ethers.utils.parseEther("500"); - await expect(talentVault.connect(user1).withdraw(withdrawAmount)) - .to.emit(talentVault, "Withdrawn") - .withArgs(user1.address, withdrawAmount); - - const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.be.closeTo(ethers.utils.parseEther("500"), ethers.utils.parseEther("0.1")); - }); - - it("Should not allow withdrawals of more than the balance", async () => { - const withdrawAmount = ethers.utils.parseEther("1500"); - await expect(talentVault.connect(user1).withdraw(withdrawAmount)).to.be.revertedWith("Not enough balance"); - }); - }); - - describe("Interest Calculation", () => { - it("Should calculate interest correctly", async () => { - const depositAmount = ethers.utils.parseEther("1000"); - await talentToken.transfer(user1.address, depositAmount); - await talentToken.connect(user1).approve(talentVault.address, depositAmount); - await talentVault.connect(user1).deposit(depositAmount); - - // Simulate time passing - await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - await ethers.provider.send("evm_mine", []); - - const expectedInterest = depositAmount.mul(10).div(100); // 10% interest - const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.equal(depositAmount.add(expectedInterest)); - }); - - // 10000 - it("Should calculate interest even if amount is above the max yield amount correctly", async () => { - const depositAmount = ethers.utils.parseEther("15000"); - const maxAmount = ethers.utils.parseEther("10000"); - await talentToken.transfer(user1.address, depositAmount); - await talentToken.connect(user1).approve(talentVault.address, depositAmount); - await talentVault.connect(user1).deposit(depositAmount); - - // Simulate time passing - await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - await ethers.provider.send("evm_mine", []); - - const expectedInterest = maxAmount.mul(10).div(100); // 10% interest - const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.equal(depositAmount.add(expectedInterest)); - }); - - it("Should calculate interest correctly for builders with scores below 50", async () => { - await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode - await passportRegistry.connect(user1).create("source1"); - - const passportId = await passportRegistry.passportId(user1.address); - await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 - const depositAmount = ethers.utils.parseEther("1000"); - await talentToken.transfer(user1.address, depositAmount); - await talentToken.connect(user1).approve(talentVault.address, depositAmount); - await talentVault.connect(user1).deposit(depositAmount); - - // Simulate time passing - await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - await ethers.provider.send("evm_mine", []); - - await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 - - const expectedInterest = depositAmount.mul(15).div(100); // 15% interest - const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); - }); - - it("Should calculate interest correctly for builders with scores above 50", async () => { - await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode - await passportRegistry.connect(user1).create("source1"); - - const passportId = await passportRegistry.passportId(user1.address); - await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 - const depositAmount = ethers.utils.parseEther("1000"); - await talentToken.transfer(user1.address, depositAmount); - await talentToken.connect(user1).approve(talentVault.address, depositAmount); - await talentVault.connect(user1).deposit(depositAmount); - - // Simulate time passing - await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - await ethers.provider.send("evm_mine", []); - - await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 - - const expectedInterest = depositAmount.mul(20).div(100); // 20% interest - const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); - }); - - it("Should calculate interest correctly for builders with scores above 75", async () => { - await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode - await passportRegistry.connect(user1).create("source1"); - - const passportId = await passportRegistry.passportId(user1.address); - await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 - const depositAmount = ethers.utils.parseEther("1000"); - await talentToken.transfer(user1.address, depositAmount); - await talentToken.connect(user1).approve(talentVault.address, depositAmount); - await talentVault.connect(user1).deposit(depositAmount); - - // Simulate time passing - await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - await ethers.provider.send("evm_mine", []); - - await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 - - const expectedInterest = depositAmount.mul(25).div(100); // 25% interest - const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + describe("#refreshForAddress", async () => { + context("when address does not have a deposit", async () => { + it("reverts", async () => { + await expect(talentVault.refreshForAddress(user1.address)).to.be.revertedWith("NoDepositFound"); + }); }); }); - describe("Administrative Functions", () => { - it("Should allow the owner to update the yield rate", async () => { - const newYieldRate = 15_00; // 15% - await talentVault.connect(admin).setYieldRate(newYieldRate); - expect(await talentVault.yieldRateBase()).to.equal(newYieldRate); - }); - - it("Should not allow non-owners to update the yield rate", async () => { - const newYieldRate = 15_00; // 15% - await expect(talentVault.connect(user1).setYieldRate(newYieldRate)).to.be.revertedWith( - `OwnableUnauthorizedAccount("${user1.address}")` - ); - }); - }); + // describe("Withdrawals", () => { + // beforeEach(async () => { + // const depositAmount = ethers.utils.parseEther("1000"); + // await talentToken.transfer(user1.address, depositAmount); + // await talentToken.connect(user1).approve(talentVault.address, depositAmount); + // await talentVault.connect(user1).deposit(depositAmount); + // }); + + // it("Should allow users to withdraw tokens", async () => { + // const withdrawAmount = ethers.utils.parseEther("500"); + // await expect(talentVault.connect(user1).withdraw(withdrawAmount)) + // .to.emit(talentVault, "Withdrawn") + // .withArgs(user1.address, withdrawAmount); + + // const userBalance = await talentVault.balanceOf(user1.address); + // expect(userBalance).to.be.closeTo(ethers.utils.parseEther("500"), ethers.utils.parseEther("0.1")); + // }); + + // it("Should not allow withdrawals of more than the balance", async () => { + // const withdrawAmount = ethers.utils.parseEther("1500"); + // await expect(talentVault.connect(user1).withdraw(withdrawAmount)).to.be.revertedWith("Not enough balance"); + // }); + // }); + + // describe("Interest Calculation", () => { + // it("Should calculate interest correctly", async () => { + // const depositAmount = ethers.utils.parseEther("1000"); + // await talentToken.transfer(user1.address, depositAmount); + // await talentToken.connect(user1).approve(talentVault.address, depositAmount); + // await talentVault.connect(user1).deposit(depositAmount); + + // // Simulate time passing + // await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + // await ethers.provider.send("evm_mine", []); + + // const expectedInterest = depositAmount.mul(10).div(100); // 10% interest + // const userBalance = await talentVault.balanceOf(user1.address); + // expect(userBalance).to.equal(depositAmount.add(expectedInterest)); + // }); + + // // 10000 + // it("Should calculate interest even if amount is above the max yield amount correctly", async () => { + // const depositAmount = ethers.utils.parseEther("15000"); + // const maxAmount = ethers.utils.parseEther("10000"); + // await talentToken.transfer(user1.address, depositAmount); + // await talentToken.connect(user1).approve(talentVault.address, depositAmount); + // await talentVault.connect(user1).deposit(depositAmount); + + // // Simulate time passing + // await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + // await ethers.provider.send("evm_mine", []); + + // const expectedInterest = maxAmount.mul(10).div(100); // 10% interest + // const userBalance = await talentVault.balanceOf(user1.address); + // expect(userBalance).to.equal(depositAmount.add(expectedInterest)); + // }); + + // it("Should calculate interest correctly for builders with scores below 50", async () => { + // await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode + // await passportRegistry.connect(user1).create("source1"); + + // const passportId = await passportRegistry.passportId(user1.address); + // await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 + // const depositAmount = ethers.utils.parseEther("1000"); + // await talentToken.transfer(user1.address, depositAmount); + // await talentToken.connect(user1).approve(talentVault.address, depositAmount); + // await talentVault.connect(user1).deposit(depositAmount); + + // // Simulate time passing + // await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + // await ethers.provider.send("evm_mine", []); + + // await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 + + // const expectedInterest = depositAmount.mul(15).div(100); // 15% interest + // const userBalance = await talentVault.balanceOf(user1.address); + // expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + // }); + + // it("Should calculate interest correctly for builders with scores above 50", async () => { + // await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode + // await passportRegistry.connect(user1).create("source1"); + + // const passportId = await passportRegistry.passportId(user1.address); + // await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 + // const depositAmount = ethers.utils.parseEther("1000"); + // await talentToken.transfer(user1.address, depositAmount); + // await talentToken.connect(user1).approve(talentVault.address, depositAmount); + // await talentVault.connect(user1).deposit(depositAmount); + + // // Simulate time passing + // await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + // await ethers.provider.send("evm_mine", []); + + // await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 + + // const expectedInterest = depositAmount.mul(20).div(100); // 20% interest + // const userBalance = await talentVault.balanceOf(user1.address); + // expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + // }); + + // it("Should calculate interest correctly for builders with scores above 75", async () => { + // await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode + // await passportRegistry.connect(user1).create("source1"); + + // const passportId = await passportRegistry.passportId(user1.address); + // await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 + // const depositAmount = ethers.utils.parseEther("1000"); + // await talentToken.transfer(user1.address, depositAmount); + // await talentToken.connect(user1).approve(talentVault.address, depositAmount); + // await talentVault.connect(user1).deposit(depositAmount); + + // // Simulate time passing + // await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + // await ethers.provider.send("evm_mine", []); + + // await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 + + // const expectedInterest = depositAmount.mul(25).div(100); // 25% interest + // const userBalance = await talentVault.balanceOf(user1.address); + // expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + // }); + // }); + + // describe("Administrative Functions", () => { + // it("Should allow the owner to update the yield rate", async () => { + // const newYieldRate = 15_00; // 15% + // await talentVault.connect(admin).setYieldRate(newYieldRate); + // expect(await talentVault.yieldRateBase()).to.equal(newYieldRate); + // }); + + // it("Should not allow non-owners to update the yield rate", async () => { + // const newYieldRate = 15_00; // 15% + // await expect(talentVault.connect(user1).setYieldRate(newYieldRate)).to.be.revertedWith( + // `OwnableUnauthorizedAccount("${user1.address}")` + // ); + // }); + // }); }); From 3871b8663b0092396571269759acf5b784e87e75 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Wed, 30 Oct 2024 21:21:22 +0200 Subject: [PATCH 33/74] Now with more tests and better calculation logic --- contracts/talent/TalentVault.sol | 95 ++----- test/contracts/talent/TalentVault.ts | 397 ++++++++++++--------------- 2 files changed, 198 insertions(+), 294 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 74d2caa1..1e13f3eb 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -8,6 +8,8 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "../passport/PassportBuilderScore.sol"; +import "hardhat/console.sol"; + error ContractInsolvent(); error InsufficientAllowance(); error InsufficientBalance(); @@ -116,9 +118,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { token = _token; yieldRateBase = 10_00; - yieldRateProficient = 15_00; - yieldRateCompetent = 20_00; - yieldRateExpert = 25_00; yieldSource = _yieldSource; maxYieldAmount = _maxYieldAmount; passportBuilderScore = _passportBuilderScore; @@ -159,6 +158,8 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { revert InvalidDepositAmount(); } + refreshForAddress(receiver); + uint256 shares = super.deposit(assets, receiver); UserBalanceMeta storage balanceMeta = userBalanceMeta[receiver]; @@ -168,18 +169,12 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { // +receiver+ wallet to +address(this)+ wallet. // But this one here, it gives us an easy way to find out // how much $TALENT a +receiveruserBalanceMetadeposited. + balanceMeta.depositedAmount += assets; return shares; } - /// @notice UserDeposit tokens into a user's account, which will start accruing interest. - /// @param account The address of the user to deposit tokens fo - /// @param amount The amount of tokens to deposit - function depositForAddress(address account, uint256 amount) public { - deposit(amount, account); - } - function mint(uint256 shares, address receiver) public virtual override returns (uint256) { return deposit(shares, receiver); } @@ -188,7 +183,9 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @param account The address of the user to refresh function refreshForAddress(address account) public { if (balanceOf(account) <= 0) { - revert NoDepositFound(); + UserBalanceMeta storage balanceMeta = userBalanceMeta[account]; + balanceMeta.lastInterestCalculation = block.timestamp; + return; } yieldInterest(account); @@ -199,40 +196,10 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { refreshForAddress(msg.sender); } - /// @notice Withdraws the requested amount from the user's balance. - // function withdraw(uint256 amount) external nonReentrant { - // _withdraw(msg.sender, amount); - // } - - // function withdraw( - // uint256 assets, - // address receiver, - // address owner - // ) public virtual override returns (uint256 shares) { - // return super.withdraw(assets, receiver, owner); - // } - /// @notice Withdraws all of the user's balance, including any accrued interest. function withdrawAll() external nonReentrant { - _withdraw(msg.sender, balanceOf(msg.sender)); - } - - function recoverDeposit() external { - // UserDeposit storage userDeposit = getDeposit[msg.sender]; - // if (userDeposit.amount <= 0) { - // revert NoDepositFound(); - // } - // refreshInterest(userDeposit); - // uint256 amount = userDeposit.depositedAmount; - // userDeposit.amount -= amount; - // userDeposit.depositedAmount = 0; - // if (token.balanceOf(address(this)) < amount) { - // revert ContractInsolvent(); - // } - // try token.transfer(msg.sender, amount) {} catch { - // revert TransferFailed(); - // } - // emit Withdrawn(msg.sender, amount); + refreshForAddress(msg.sender); + redeem(balanceOf(msg.sender), msg.sender, msg.sender); } /// @notice Update the yield rate for the contract @@ -251,9 +218,9 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { uint256 builderScore = passportBuilderScore.getScore(passportId); if (builderScore < 25) return yieldRateBase; - if (builderScore < 50) return yieldRateProficient; - if (builderScore < 75) return yieldRateCompetent; - return yieldRateExpert; + if (builderScore < 50) return yieldRateBase + 5_00; + if (builderScore < 75) return yieldRateBase + 10_00; + return yieldRateBase + 15_00; } /// @notice Update the maximum amount of tokens that can be used to calculate interest @@ -303,11 +270,17 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { function calculateInterest(address user) internal returns (uint256) { uint256 userBalance = balanceOf(user); + console.log("userBalance %d", userBalance); + console.log("maxYieldAmount %d", maxYieldAmount); + if (userBalance > maxYieldAmount) { userBalance = maxYieldAmount; } uint256 endTime; + + console.log("yieldAccrualDeadline %d", yieldAccrualDeadline); + if (yieldAccrualDeadline != 0 && block.timestamp > yieldAccrualDeadline) { endTime = yieldAccrualDeadline; } else { @@ -328,6 +301,9 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { uint256 yieldRate = getYieldRateForScore(user); + console.log("yieldRate %d", yieldRate); + console.log("timeElapsed %d", timeElapsed); + balanceMeta.lastInterestCalculation = block.timestamp; return (userBalance * yieldRate * timeElapsed) / (SECONDS_PER_YEAR_x_ONE_HUNDRED_PERCENT); @@ -337,31 +313,8 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { function yieldInterest(address user) internal { uint256 interest = calculateInterest(user); - _deposit(yieldSource, user, interest, interest); - } + console.log("interest %s", interest); - /// @dev Withdraws the user's balance, including any accrued interest - /// @param user The address of the user to withdraw the balance of - /// @param amount The amount of tokens to withdraw - function _withdraw(address user, uint256 amount) internal { - // UserDeposit storage userDeposit = getDeposit[user]; - // if (userDeposit.amount <= 0) { - // revert NoDepositFound(); - // } - // refreshInterest(userDeposit); - // require(userDeposit.amount >= amount, "Not enough balance"); - // uint256 contractBalance = token.balanceOf(address(this)); - // uint256 fromContractAmount = amount < userDeposit.depositedAmount ? amount : userDeposit.depositedAmount; - // uint256 fromYieldSourceAmount = amount - fromContractAmount; - // require(contractBalance >= fromContractAmount, "Contract insolvent"); - // userDeposit.amount -= amount; - // userDeposit.depositedAmount -= fromContractAmount; - // emit Withdrawn(user, amount); - // if (fromContractAmount > 0) { - // require(token.transfer(user, fromContractAmount), "Transfer failed"); - // } - // if (fromYieldSourceAmount > 0) { - // require(token.transferFrom(yieldSource, user, fromYieldSourceAmount), "Transfer failed"); - // } + _deposit(yieldSource, user, interest, interest); } } diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index bba9882a..322632c7 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -40,7 +40,6 @@ describe("TalentVault", () => { admin.address, ethers.utils.parseEther("10000"), passportBuilderScore.address, - // adminInitialDeposit, ])) as TalentVault; console.log("------------------------------------"); @@ -59,7 +58,7 @@ describe("TalentVault", () => { await talentToken.unpause(); // just make sure that TV wallet has $TALENT as initial assets from admin initial deposit - await talentToken.approve(talentVault.address, adminInitialDeposit); + await talentToken.approve(talentVault.address, ethers.constants.MaxUint256); await talentVault.mint(adminInitialDeposit, admin.address); // await talentToken.renounceOwnership(); @@ -73,9 +72,6 @@ describe("TalentVault", () => { it("Should set the correct initial values", async () => { expect(await talentVault.yieldRateBase()).to.equal(10_00); - expect(await talentVault.yieldRateProficient()).to.equal(15_00); - expect(await talentVault.yieldRateCompetent()).to.equal(20_00); - expect(await talentVault.yieldRateExpert()).to.equal(25_00); expect(await talentVault.maxYieldAmount()).to.equal(ethers.utils.parseEther("10000")); @@ -117,19 +113,6 @@ describe("TalentVault", () => { }); }); - // TODO: Do we want this? Does it - // depreciate the reliability of the contract at - // the eyes of our users? - // - describe("Pausable", async () => { - it("is Pausable"); - it("when Pausable, we cannot deposit"); - it("when Pausable, we cannot withdraw"); - it("when paused we cannot .... other ..."); - it("can be paused only by the owner"); - it("can be unpaused only by the owner"); - }); - // TODO: StopYieldingInterest describe("#name", async () => { @@ -273,46 +256,87 @@ describe("TalentVault", () => { describe("#deposit", async () => { it("Should mint $TALENTVAULT to the given receiver, equally increase the TalentVault $TALENT balance and equally decreases the $TALENT balance of receiver", async () => { - const depositAmountInTalent = 10_000n; - const equivalentDepositAmountInTalentVault = depositAmountInTalent; - - await talentToken.connect(user1).approve(talentVault.address, depositAmountInTalent); - await talentToken.transfer(user1.address, depositAmountInTalent); // so that it has enough balance + const depositAmount = 100_000n; + await talentToken.transfer(user1.address, depositAmount); // so that sender has enough balance const user1BalanceBefore = await talentToken.balanceOf(user1.address); - const user1BalanceInTalentVaultBefore = await talentVault.balanceOf(user1.address); + + await talentToken.connect(user1).approve(talentVault.address, depositAmount); // so that sender has approved vault + const vaultBalanceBefore = await talentToken.balanceOf(talentVault.address); - const userBalanceMetaBefore = await talentVault.userBalanceMeta(user1.address); - const depositedAmountBefore = userBalanceMetaBefore.depositedAmount; - // fire (admin deposits to itself) - await expect(talentVault.connect(user1).deposit(depositAmountInTalent, user1.address)) + const user2BalanceMetaBefore = await talentVault.userBalanceMeta(user2.address); + + const user2TalentVaultBalanceBefore = await talentVault.balanceOf(user2.address); + + // fire + await expect(talentVault.connect(user1).deposit(depositAmount, user2.address)) .to.emit(talentVault, "Deposit") - .withArgs(user1.address, user1.address, depositAmountInTalent, equivalentDepositAmountInTalentVault); + .withArgs(user1.address, user2.address, depositAmount, depositAmount); - // vault balance in TALENT is increased + // user1 $TALENT balance is decreased + const user1BalanceAfter = await talentToken.balanceOf(user1.address); + const expectedUser1BalanceAfter = user1BalanceBefore.sub(depositAmount); + expect(user1BalanceAfter).to.equal(expectedUser1BalanceAfter); + + // vault $TALENT balance is increased const vaultBalanceAfter = await talentToken.balanceOf(talentVault.address); - const expectedVaultBalanceAfter = vaultBalanceBefore.toBigInt() + depositAmountInTalent; + const expectedVaultBalanceAfter = vaultBalanceBefore.toBigInt() + depositAmount; expect(vaultBalanceAfter).to.equal(expectedVaultBalanceAfter); - // user1 balance in TALENT decreases - const user1BalanceAfter = await talentToken.balanceOf(user1.address); - expect(user1BalanceAfter).to.equal(user1BalanceBefore.toBigInt() - depositAmountInTalent); - - // user1 balance in TalentVault increases (mint result) - const user1BalanceInTalentVaultAfter = await talentVault.balanceOf(user1.address); - expect(user1BalanceInTalentVaultAfter).to.equal( - user1BalanceInTalentVaultBefore.toBigInt() + equivalentDepositAmountInTalentVault + // deposit for user2 is updated on storage + const user2BalanceMetaAfter = await talentVault.userBalanceMeta(user2.address); + expect(user2BalanceMetaAfter.depositedAmount).to.equal( + user2BalanceMetaBefore.depositedAmount.toBigInt() + depositAmount ); - // user1 depositedAmount is increased - const userBalanceMeta = await talentVault.userBalanceMeta(user1.address); - const depositedAmountAfter = userBalanceMeta.depositedAmount; - expect(depositedAmountAfter).to.equal(depositedAmountBefore.toBigInt() + equivalentDepositAmountInTalentVault); + // user2 $TALENTVAULT balance is increased + const user2TalentVaultBalanceAfter = await talentVault.balanceOf(user2.address); + expect(user2TalentVaultBalanceAfter).to.equal(user2TalentVaultBalanceBefore.toBigInt() + depositAmount); }); it("Should revert if $TALENT deposited is 0", async () => { await expect(talentVault.connect(user1).deposit(0n, user1.address)).to.be.revertedWith("InvalidDepositAmount"); }); + + it("Should not allow deposit of amount that the sender does not have", async () => { + const balanceOfUser1 = 100_000n; + + await talentToken.transfer(user1.address, balanceOfUser1); + + const depositAmount = 100_001n; + + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + + await expect(talentVault.connect(user1).deposit(depositAmount, user2.address)).to.be.revertedWith( + "ERC20InsufficientBalance" + ); + }); + + it("Should not allow deposit of amount bigger than the allowed by the sender to be spent by the talent contract", async () => { + const depositAmount = 100_000n; + + await talentToken.transfer(user1.address, depositAmount); // so that user1 has enough balance + + const approvedAmount = depositAmount - 1n; + + await talentToken.connect(user1).approve(talentVault.address, approvedAmount); + + // fire + + await expect(talentVault.connect(user1).deposit(depositAmount, user2.address)).to.be.revertedWith( + "ERC20InsufficientAllowance" + ); + }); + + it("Should allow deposit of amount equal to the allowed by the sender to be spent by the talent contract", async () => { + const depositAmount = ethers.utils.parseEther("1000"); + + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + + await expect(talentVault.connect(user1).deposit(depositAmount, user2.address)).to.be.revertedWith( + "ERC20InsufficientBalance" + ); + }); }); describe("#setMaxMint", async () => { @@ -451,7 +475,7 @@ describe("TalentVault", () => { }); }); - describe("#withDraw", async () => { + describe("#withdraw", async () => { it("burns $TALENTVAULT from owner, increases $TALENT balance of receiver, decreases $TALENT balance of TalentVault", async () => { const depositTalent = 10_000n; @@ -550,224 +574,151 @@ describe("TalentVault", () => { }); }); - describe("#depositForAddress", async () => { - it("Should deposit the amount to the address given", async () => { - const depositAmount = 100_000n; - await talentToken.transfer(user1.address, depositAmount); // so that sender has enough balance - const user1BalanceBefore = await talentToken.balanceOf(user1.address); - - await talentToken.connect(user1).approve(talentVault.address, depositAmount); // so that sender has approved vault - - const vaultBalanceBefore = await talentToken.balanceOf(talentVault.address); - - const user2BalanceMetaBefore = await talentVault.userBalanceMeta(user2.address); - - const user2TalentVaultBalanceBefore = await talentVault.balanceOf(user2.address); - - // fire - await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)) - .to.emit(talentVault, "Deposit") - .withArgs(user1.address, user2.address, depositAmount, depositAmount); - - // user1 $TALENT balance is decreased - const user1BalanceAfter = await talentToken.balanceOf(user1.address); - const expectedUser1BalanceAfter = user1BalanceBefore.sub(depositAmount); - expect(user1BalanceAfter).to.equal(expectedUser1BalanceAfter); + describe("#refreshForAddress", async () => { + context("when address does not have a deposit", async () => { + it("just updates the last interest calculation", async () => { + const lastInterestCalculationBefore = (await talentVault.userBalanceMeta(user3.address)) + .lastInterestCalculation; - // vault $TALENT balance is increased - const vaultBalanceAfter = await talentToken.balanceOf(talentVault.address); - const expectedVaultBalanceAfter = vaultBalanceBefore.toBigInt() + depositAmount; - expect(vaultBalanceAfter).to.equal(expectedVaultBalanceAfter); + expect(lastInterestCalculationBefore).to.equal(0); - // deposit for user2 is updated on storage - const user2BalanceMetaAfter = await talentVault.userBalanceMeta(user2.address); - expect(user2BalanceMetaAfter.depositedAmount).to.equal( - user2BalanceMetaBefore.depositedAmount.toBigInt() + depositAmount - ); + // fire + await talentVault.refreshForAddress(user3.address); - // user2 $TALENTVAULT balance is increased - const user2TalentVaultBalanceAfter = await talentVault.balanceOf(user2.address); - expect(user2TalentVaultBalanceAfter).to.equal(user2TalentVaultBalanceBefore.toBigInt() + depositAmount); - }); + const lastInterestCalculation = (await talentVault.userBalanceMeta(user3.address)).lastInterestCalculation; - it("Should not allow deposits of zero tokens", async () => { - await expect(talentVault.connect(user1).depositForAddress(ethers.constants.AddressZero, 0n)).to.be.revertedWith( - "InvalidDepositAmount()" - ); + expect(lastInterestCalculation).not.to.equal(0); + }); }); - it("Should not allow deposit of amount that the sender does not have", async () => { - const balanceOfUser1 = 100_000n; - - await talentToken.transfer(user1.address, balanceOfUser1); + // Make sure user balance is updated according to yielded interest + }); - const depositAmount = 100_001n; + // withdrawAll + // + // $TALENT for user is increased by their $TALENTVAULT balance + // which is updated with the yield interest. + // + // TalentVault $TALENT balance is reduced by the amount that is withdrawn + // + // user $TALENTVAULT balance goes to 0. + describe("Interest Calculation", () => { + it("Should calculate interest correctly", async () => { + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount, user1.address); - await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)).to.be.revertedWith( - "ERC20InsufficientBalance" - ); - }); - - it("Should not allow deposit of amount bigger than the allowed by the sender to be spent by the talent contract", async () => { - const depositAmount = 100_000n; - - await talentToken.transfer(user1.address, depositAmount); // so that user1 has enough balance - - const approvedAmount = depositAmount - 1n; + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); - await talentToken.connect(user1).approve(talentVault.address, approvedAmount); + const expectedInterest = depositAmount.mul(10).div(100); // 10% interest // fire + await talentVault.connect(user1).refresh(); - await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)).to.be.revertedWith( - "ERC20InsufficientAllowance" - ); + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.001")); }); - it("Should allow deposit of amount equal to the allowed by the sender to be spent by the talent contract", async () => { - const depositAmount = ethers.utils.parseEther("1000"); - + // 10000 + it("Should calculate interest even if amount is above the max yield amount correctly", async () => { + const depositAmount = ethers.utils.parseEther("15000"); + const maxAmount = ethers.utils.parseEther("10000"); + await talentToken.transfer(user1.address, depositAmount); await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount, user1.address); - await expect(talentVault.connect(user1).depositForAddress(user2.address, depositAmount)).to.be.revertedWith( - "ERC20InsufficientBalance" - ); - }); - }); - - describe("#refreshForAddress", async () => { - context("when address does not have a deposit", async () => { - it("reverts", async () => { - await expect(talentVault.refreshForAddress(user1.address)).to.be.revertedWith("NoDepositFound"); - }); - }); - }); - - // describe("Withdrawals", () => { - // beforeEach(async () => { - // const depositAmount = ethers.utils.parseEther("1000"); - // await talentToken.transfer(user1.address, depositAmount); - // await talentToken.connect(user1).approve(talentVault.address, depositAmount); - // await talentVault.connect(user1).deposit(depositAmount); - // }); - - // it("Should allow users to withdraw tokens", async () => { - // const withdrawAmount = ethers.utils.parseEther("500"); - // await expect(talentVault.connect(user1).withdraw(withdrawAmount)) - // .to.emit(talentVault, "Withdrawn") - // .withArgs(user1.address, withdrawAmount); + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); - // const userBalance = await talentVault.balanceOf(user1.address); - // expect(userBalance).to.be.closeTo(ethers.utils.parseEther("500"), ethers.utils.parseEther("0.1")); - // }); + const expectedInterest = maxAmount.mul(10).div(100); // 10% interest - // it("Should not allow withdrawals of more than the balance", async () => { - // const withdrawAmount = ethers.utils.parseEther("1500"); - // await expect(talentVault.connect(user1).withdraw(withdrawAmount)).to.be.revertedWith("Not enough balance"); - // }); - // }); + // fire + await talentVault.connect(user1).refresh(); - // describe("Interest Calculation", () => { - // it("Should calculate interest correctly", async () => { - // const depositAmount = ethers.utils.parseEther("1000"); - // await talentToken.transfer(user1.address, depositAmount); - // await talentToken.connect(user1).approve(talentVault.address, depositAmount); - // await talentVault.connect(user1).deposit(depositAmount); + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.001")); + }); - // // Simulate time passing - // await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - // await ethers.provider.send("evm_mine", []); + it("Should calculate interest correctly for builders with scores below 50", async () => { + await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode + await passportRegistry.connect(user1).create("source1"); - // const expectedInterest = depositAmount.mul(10).div(100); // 10% interest - // const userBalance = await talentVault.balanceOf(user1.address); - // expect(userBalance).to.equal(depositAmount.add(expectedInterest)); - // }); + const passportId = await passportRegistry.passportId(user1.address); + await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount, user1.address); - // // 10000 - // it("Should calculate interest even if amount is above the max yield amount correctly", async () => { - // const depositAmount = ethers.utils.parseEther("15000"); - // const maxAmount = ethers.utils.parseEther("10000"); - // await talentToken.transfer(user1.address, depositAmount); - // await talentToken.connect(user1).approve(talentVault.address, depositAmount); - // await talentVault.connect(user1).deposit(depositAmount); - - // // Simulate time passing - // await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - // await ethers.provider.send("evm_mine", []); - - // const expectedInterest = maxAmount.mul(10).div(100); // 10% interest - // const userBalance = await talentVault.balanceOf(user1.address); - // expect(userBalance).to.equal(depositAmount.add(expectedInterest)); - // }); + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); - // it("Should calculate interest correctly for builders with scores below 50", async () => { - // await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode - // await passportRegistry.connect(user1).create("source1"); + await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 - // const passportId = await passportRegistry.passportId(user1.address); - // await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 - // const depositAmount = ethers.utils.parseEther("1000"); - // await talentToken.transfer(user1.address, depositAmount); - // await talentToken.connect(user1).approve(talentVault.address, depositAmount); - // await talentVault.connect(user1).deposit(depositAmount); + // fire + await talentVault.connect(user1).refresh(); - // // Simulate time passing - // await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - // await ethers.provider.send("evm_mine", []); + const expectedInterest = depositAmount.mul(15).div(100); // 15% interest + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + }); - // await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 + it("Should calculate interest correctly for builders with scores above 50", async () => { + await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode + await passportRegistry.connect(user1).create("source1"); - // const expectedInterest = depositAmount.mul(15).div(100); // 15% interest - // const userBalance = await talentVault.balanceOf(user1.address); - // expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); - // }); + const passportId = await passportRegistry.passportId(user1.address); + await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount, user1.address); - // it("Should calculate interest correctly for builders with scores above 50", async () => { - // await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode - // await passportRegistry.connect(user1).create("source1"); + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); - // const passportId = await passportRegistry.passportId(user1.address); - // await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 - // const depositAmount = ethers.utils.parseEther("1000"); - // await talentToken.transfer(user1.address, depositAmount); - // await talentToken.connect(user1).approve(talentVault.address, depositAmount); - // await talentVault.connect(user1).deposit(depositAmount); + await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 - // // Simulate time passing - // await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - // await ethers.provider.send("evm_mine", []); + // fire + await talentVault.connect(user1).refresh(); - // await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 + const expectedInterest = depositAmount.mul(20).div(100); // 20% interest + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + }); - // const expectedInterest = depositAmount.mul(20).div(100); // 20% interest - // const userBalance = await talentVault.balanceOf(user1.address); - // expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); - // }); + it("Should calculate interest correctly for builders with scores above 75", async () => { + await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode + await passportRegistry.connect(user1).create("source1"); - // it("Should calculate interest correctly for builders with scores above 75", async () => { - // await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode - // await passportRegistry.connect(user1).create("source1"); + const passportId = await passportRegistry.passportId(user1.address); + await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount, user1.address); - // const passportId = await passportRegistry.passportId(user1.address); - // await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 - // const depositAmount = ethers.utils.parseEther("1000"); - // await talentToken.transfer(user1.address, depositAmount); - // await talentToken.connect(user1).approve(talentVault.address, depositAmount); - // await talentVault.connect(user1).deposit(depositAmount); + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); - // // Simulate time passing - // await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - // await ethers.provider.send("evm_mine", []); + await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 - // await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 + // fire + await talentVault.connect(user1).refresh(); - // const expectedInterest = depositAmount.mul(25).div(100); // 25% interest - // const userBalance = await talentVault.balanceOf(user1.address); - // expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); - // }); - // }); + const expectedInterest = depositAmount.mul(25).div(100); // 25% interest + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + }); + }); // describe("Administrative Functions", () => { // it("Should allow the owner to update the yield rate", async () => { From 8f9a26bcf2cee772752118ded0445961e892a095 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 09:00:53 +0200 Subject: [PATCH 34/74] TalentVault#withdrawAll() tested --- contracts/talent/TalentVault.sol | 10 ---- test/contracts/talent/TalentVault.ts | 80 ++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 15 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 1e13f3eb..32883fde 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -270,17 +270,12 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { function calculateInterest(address user) internal returns (uint256) { uint256 userBalance = balanceOf(user); - console.log("userBalance %d", userBalance); - console.log("maxYieldAmount %d", maxYieldAmount); - if (userBalance > maxYieldAmount) { userBalance = maxYieldAmount; } uint256 endTime; - console.log("yieldAccrualDeadline %d", yieldAccrualDeadline); - if (yieldAccrualDeadline != 0 && block.timestamp > yieldAccrualDeadline) { endTime = yieldAccrualDeadline; } else { @@ -301,9 +296,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { uint256 yieldRate = getYieldRateForScore(user); - console.log("yieldRate %d", yieldRate); - console.log("timeElapsed %d", timeElapsed); - balanceMeta.lastInterestCalculation = block.timestamp; return (userBalance * yieldRate * timeElapsed) / (SECONDS_PER_YEAR_x_ONE_HUNDRED_PERCENT); @@ -313,8 +305,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { function yieldInterest(address user) internal { uint256 interest = calculateInterest(user); - console.log("interest %s", interest); - _deposit(yieldSource, user, interest, interest); } } diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 322632c7..dd845d44 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -7,6 +7,7 @@ import { TalentProtocolToken, TalentVault, PassportRegistry, PassportBuilderScor import { Artifacts } from "../../shared"; import { TalentVault as TalentVaultArtifact } from "../../shared/artifacts"; import { talent } from "../../../typechain-types/contracts"; +import { utils } from "ethers"; chai.use(solidity); @@ -15,6 +16,7 @@ const { deployContract } = waffle; describe("TalentVault", () => { let admin: SignerWithAddress; + let yieldSource: SignerWithAddress; let user1: SignerWithAddress; let user2: SignerWithAddress; let user3: SignerWithAddress; @@ -25,7 +27,7 @@ describe("TalentVault", () => { let talentVault: TalentVault; beforeEach(async () => { - [admin, user1, user2, user3] = await ethers.getSigners(); + [admin, yieldSource, user1, user2, user3] = await ethers.getSigners(); talentToken = (await deployContract(admin, Artifacts.TalentProtocolToken, [admin.address])) as TalentProtocolToken; passportRegistry = (await deployContract(admin, Artifacts.PassportRegistry, [admin.address])) as PassportRegistry; @@ -34,10 +36,10 @@ describe("TalentVault", () => { admin.address, ])) as PassportBuilderScore; - const adminInitialDeposit = ethers.utils.parseEther("20000"); + const adminInitialDeposit = ethers.utils.parseEther("200000"); talentVault = (await deployContract(admin, Artifacts.TalentVault, [ talentToken.address, - admin.address, + yieldSource.address, ethers.utils.parseEther("10000"), passportBuilderScore.address, ])) as TalentVault; @@ -61,6 +63,10 @@ describe("TalentVault", () => { await talentToken.approve(talentVault.address, ethers.constants.MaxUint256); await talentVault.mint(adminInitialDeposit, admin.address); + // fund the yieldSource with lots of TALENT Balance + await talentToken.transfer(yieldSource.address, ethers.utils.parseEther("100000")); + await talentToken.connect(yieldSource).approve(talentVault.address, ethers.utils.parseEther("100000")); + // await talentToken.renounceOwnership(); }); @@ -591,7 +597,8 @@ describe("TalentVault", () => { }); }); - // Make sure user balance is updated according to yielded interest + // Make sure user balance is updated according to yielded interest. This is done in the + // tests below, in the Interest Calculation tests, where we call #refresh }); // withdrawAll @@ -599,10 +606,72 @@ describe("TalentVault", () => { // $TALENT for user is increased by their $TALENTVAULT balance // which is updated with the yield interest. // - // TalentVault $TALENT balance is reduced by the amount that is withdrawn + // TalentVault $TALENT balance is reduced by the originally deposited amount + // + // yieldSource $TALENT balance is reduced by the yieldInterest // // user $TALENTVAULT balance goes to 0. + describe("#withdrawAll", async () => { + it("withdraw all the $TALENTVAULT and converts them to $TALENT", async () => { + const depositAmount = ethers.utils.parseEther("1000"); + // from admin we make user1 have some $TALENT + await talentToken.transfer(user1.address, depositAmount); + // user1 approves talentVault to spend $TALENT + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + // user1 deposits to TalentVault + // This makes user1 $TALENT to be decreased by depositAmount + // and TalentVault $TALENT to be increased by depositAmount + // and user1 $TALENTVAULT to be increased by depositAmount + await talentVault.connect(user1).deposit(depositAmount, user1.address); + + const talentVaultTalentBalanceBefore = await talentToken.balanceOf(talentVault.address); + console.log("talentVaultTalentBalanceBefore", ethers.utils.formatEther(talentVaultTalentBalanceBefore)); + const yieldSourceTalentBalanceBefore = await talentToken.balanceOf(yieldSource.address); + + const user1TalentVaultBalanceBefore = await talentVault.balanceOf(user1.address); + console.log("user1TalentVaultBalanceBefore", ethers.utils.formatEther(user1TalentVaultBalanceBefore)); + expect(user1TalentVaultBalanceBefore).to.equal(depositAmount); + + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); + + const yieldedInterest = depositAmount.mul(10).div(100); // 10% interest + + // this is manually calculated, but it is necessary for this test. + const expectedUser1TalentVaultBalanceAfter1Year = ethers.utils.parseEther("1100.000003170979198376"); + // const expectedUserTalentVaultBalanceAfter1Year = depositAmount; + + // fire + await talentVault.connect(user1).withdrawAll(); + + // TalentVault $TALENT balance is reduced by the originally deposited amount + const talentVaultTalentBalanceAfter = await talentToken.balanceOf(talentVault.address); + const expectedTalentVaultTalentBalanceAfter = talentVaultTalentBalanceBefore.sub(depositAmount); + expect(talentVaultTalentBalanceAfter).to.equal(expectedTalentVaultTalentBalanceAfter); + + // user1 $TALENT balance is increased + const user1TalentBalanceAfter = await talentToken.balanceOf(user1.address); + expect(user1TalentBalanceAfter).to.be.closeTo( + expectedUser1TalentVaultBalanceAfter1Year, + ethers.utils.parseEther("0.001") + ); + + // user1 $TALENTVAULT balance goes to 0 + const user1TalentVaultBalanceAfter = await talentVault.balanceOf(user1.address); + expect(user1TalentVaultBalanceAfter).to.equal(0); + + // yieldSource $TALENT balance is decreased by the yieldInterest + const yieldSourceTalentBalanceAfter = await talentToken.balanceOf(yieldSource.address); + const expectedYieldSourceTalentBalanceAfter = yieldSourceTalentBalanceBefore.sub(yieldedInterest); + expect(yieldSourceTalentBalanceAfter).to.be.closeTo( + expectedYieldSourceTalentBalanceAfter, + ethers.utils.parseEther("0.001") + ); + }); + }); + describe("Interest Calculation", () => { it("Should calculate interest correctly", async () => { const depositAmount = ethers.utils.parseEther("1000"); @@ -620,6 +689,7 @@ describe("TalentVault", () => { await talentVault.connect(user1).refresh(); const userBalance = await talentVault.balanceOf(user1.address); + console.log("userBalance", ethers.utils.formatEther(userBalance)); expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.001")); }); From cecb1e4f708878cc08cf8a8e040603b1e84bd86c Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 09:04:25 +0200 Subject: [PATCH 35/74] Removed commented out code --- contracts/talent/TalentVault.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 32883fde..b655a21c 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -45,7 +45,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { struct UserBalanceMeta { uint256 depositedAmount; uint256 lastInterestCalculation; - // address user; } /// @notice The number of seconds in a year From 2bcb8de75dc95e4b06f0845f45bad9d5b7924d8d Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 09:05:42 +0200 Subject: [PATCH 36/74] Removed the import of hardhat console --- contracts/talent/TalentVault.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index b655a21c..b90a6827 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -8,8 +8,6 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "../passport/PassportBuilderScore.sol"; -import "hardhat/console.sol"; - error ContractInsolvent(); error InsufficientAllowance(); error InsufficientBalance(); From 179ec9991121e69a62d5a87b651676ec8c30f550 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 09:18:19 +0200 Subject: [PATCH 37/74] Added back the test for #setYieldRate() --- test/contracts/talent/TalentVault.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index dd845d44..757095d7 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -790,18 +790,18 @@ describe("TalentVault", () => { }); }); - // describe("Administrative Functions", () => { - // it("Should allow the owner to update the yield rate", async () => { - // const newYieldRate = 15_00; // 15% - // await talentVault.connect(admin).setYieldRate(newYieldRate); - // expect(await talentVault.yieldRateBase()).to.equal(newYieldRate); - // }); - - // it("Should not allow non-owners to update the yield rate", async () => { - // const newYieldRate = 15_00; // 15% - // await expect(talentVault.connect(user1).setYieldRate(newYieldRate)).to.be.revertedWith( - // `OwnableUnauthorizedAccount("${user1.address}")` - // ); - // }); - // }); + describe("#setYieldRate", () => { + it("Should allow the owner to update the yield rate", async () => { + const newYieldRate = 15_00; // 15% + await talentVault.connect(admin).setYieldRate(newYieldRate); + expect(await talentVault.yieldRateBase()).to.equal(newYieldRate); + }); + + it("Should not allow non-owners to update the yield rate", async () => { + const newYieldRate = 15_00; // 15% + await expect(talentVault.connect(user1).setYieldRate(newYieldRate)).to.be.revertedWith( + `OwnableUnauthorizedAccount("${user1.address}")` + ); + }); + }); }); From 138428ca291370e89338e3313a203bd66ff9f6b9 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 10:13:55 +0200 Subject: [PATCH 38/74] Stop/Start Yielding Interest --- contracts/talent/TalentVault.sol | 21 ++++++- test/contracts/talent/TalentVault.ts | 88 +++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index b90a6827..62024afb 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -81,6 +81,8 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice The time at which the users of the contract will stop accruing interest uint256 public yieldAccrualDeadline; + bool public yieldInterestFlag; + PassportBuilderScore public passportBuilderScore; /// @notice A mapping of user addresses to their deposits @@ -116,6 +118,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { token = _token; yieldRateBase = 10_00; yieldSource = _yieldSource; + yieldInterestFlag = true; maxYieldAmount = _maxYieldAmount; passportBuilderScore = _passportBuilderScore; } @@ -262,9 +265,25 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { revert TalentVaultNonTransferable(); } + function stopYieldingInterest() external onlyOwner { + yieldInterestFlag = false; + } + + function startYieldingInterest() external onlyOwner { + yieldInterestFlag = true; + } + // ---------- INTERNAL -------------------------------------- function calculateInterest(address user) internal returns (uint256) { + UserBalanceMeta storage balanceMeta = userBalanceMeta[user]; + + if (!yieldInterestFlag) { + balanceMeta.lastInterestCalculation = block.timestamp; + + return 0; + } + uint256 userBalance = balanceOf(user); if (userBalance > maxYieldAmount) { @@ -279,8 +298,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { endTime = block.timestamp; } - UserBalanceMeta storage balanceMeta = userBalanceMeta[user]; - uint256 timeElapsed; if (block.timestamp > endTime) { diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 757095d7..590223db 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -26,6 +26,8 @@ describe("TalentVault", () => { let passportBuilderScore: PassportBuilderScore; let talentVault: TalentVault; + let snapshotId: bigint; + beforeEach(async () => { [admin, yieldSource, user1, user2, user3] = await ethers.getSigners(); @@ -68,9 +70,15 @@ describe("TalentVault", () => { await talentToken.connect(yieldSource).approve(talentVault.address, ethers.utils.parseEther("100000")); // await talentToken.renounceOwnership(); + + snapshotId = await ethers.provider.send("evm_snapshot", []); }); - describe("Deployment", () => { + afterEach(async () => { + await ethers.provider.send("evm_revert", [snapshotId]); + }); + + describe("Deployment", async () => { it("Should set the right owner", async () => { expect(await talentVault.owner()).not.to.equal(ethers.constants.AddressZero); expect(await talentVault.owner()).to.equal(admin.address); @@ -83,6 +91,8 @@ describe("TalentVault", () => { expect(await talentVault.passportBuilderScore()).not.to.equal(ethers.constants.AddressZero); expect(await talentVault.passportBuilderScore()).to.equal(passportBuilderScore.address); + + expect(await talentVault.yieldInterestFlag()).to.equal(true); }); it("reverts with InvalidAddress when _token given is 0", async () => { @@ -672,7 +682,7 @@ describe("TalentVault", () => { }); }); - describe("Interest Calculation", () => { + describe("Interest Calculation", async () => { it("Should calculate interest correctly", async () => { const depositAmount = ethers.utils.parseEther("1000"); await talentToken.transfer(user1.address, depositAmount); @@ -691,6 +701,42 @@ describe("TalentVault", () => { const userBalance = await talentVault.balanceOf(user1.address); console.log("userBalance", ethers.utils.formatEther(userBalance)); expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.001")); + + const userLastInterestCalculation = (await talentVault.userBalanceMeta(user1.address)).lastInterestCalculation; + console.log("userLastInterestCalculation", userLastInterestCalculation); + const currentDateEpochSeconds = Math.floor(Date.now() / 1000); + console.log("currentDateEpochSeconds", currentDateEpochSeconds); + const oneYearAfterEpochSeconds = currentDateEpochSeconds + 31536000; + + expect(userLastInterestCalculation.toNumber()).to.be.closeTo(oneYearAfterEpochSeconds, 500); + }); + + context("when yielding interest is stopped", async () => { + it("does not yield any interest but it updates the lastInterestCalculation", async () => { + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + const user1BalanceBefore = await talentToken.balanceOf(user1.address); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + await talentVault.connect(user1).deposit(depositAmount, user1.address); + + // Simulate time passing + await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year + await ethers.provider.send("evm_mine", []); + + await talentVault.stopYieldingInterest(); + + // fire + await talentVault.connect(user1).refresh(); + + const user1BalanceAfter = await talentVault.balanceOf(user1.address); + expect(user1BalanceAfter).to.equal(user1BalanceBefore); + + const userLastInterestCalculation = (await talentVault.userBalanceMeta(user1.address)).lastInterestCalculation; + const currentDateEpochSeconds = Math.floor(Date.now() / 1000); + const oneYearAfterEpochSeconds = currentDateEpochSeconds + 31536000; + + expect(userLastInterestCalculation.toNumber()).to.be.closeTo(oneYearAfterEpochSeconds, 500); + }); }); // 10000 @@ -790,7 +836,7 @@ describe("TalentVault", () => { }); }); - describe("#setYieldRate", () => { + describe("#setYieldRate", async () => { it("Should allow the owner to update the yield rate", async () => { const newYieldRate = 15_00; // 15% await talentVault.connect(admin).setYieldRate(newYieldRate); @@ -804,4 +850,40 @@ describe("TalentVault", () => { ); }); }); + + describe("#stopYieldingInterest", async () => { + context("when called by an non-owner account", async () => { + it("reverts", async () => { + await expect(talentVault.connect(user1).stopYieldingInterest()).to.be.revertedWith( + `OwnableUnauthorizedAccount("${user1.address}")` + ); + }); + }); + + context("when called by the owner account", async () => { + it("stops yielding interest", async () => { + await talentVault.stopYieldingInterest(); + + expect(await talentVault.yieldInterestFlag()).to.equal(false); + }); + }); + }); + + describe("#startYieldingInterest", async () => { + context("when called by an non-owner account", async () => { + it("reverts", async () => { + await expect(talentVault.connect(user1).startYieldingInterest()).to.be.revertedWith( + `OwnableUnauthorizedAccount("${user1.address}")` + ); + }); + }); + + context("when called by the owner account", async () => { + it("starts yielding interest", async () => { + await talentVault.startYieldingInterest(); + + expect(await talentVault.yieldInterestFlag()).to.equal(true); + }); + }); + }); }); From 25a528b0c000558df4ce9e581529766297d2142c Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 10:18:14 +0200 Subject: [PATCH 39/74] [spec] Removed commented out code and console.log not needed --- test/contracts/talent/TalentVault.ts | 29 ++++++++++------------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 590223db..f03edea2 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -5,9 +5,6 @@ import { solidity } from "ethereum-waffle"; import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { TalentProtocolToken, TalentVault, PassportRegistry, PassportBuilderScore } from "../../../typechain-types"; import { Artifacts } from "../../shared"; -import { TalentVault as TalentVaultArtifact } from "../../shared/artifacts"; -import { talent } from "../../../typechain-types/contracts"; -import { utils } from "ethers"; chai.use(solidity); @@ -46,15 +43,16 @@ describe("TalentVault", () => { passportBuilderScore.address, ])) as TalentVault; - console.log("------------------------------------"); - console.log("Addresses:"); - console.log(`admin = ${admin.address}`); - console.log(`user1 = ${user1.address}`); - console.log(`user2 = ${user2.address}`); - console.log(`user3 = ${user3.address}`); - console.log(`talentToken = ${talentToken.address}`); - console.log(`talentVault = ${talentVault.address}`); - console.log("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); + // ------------- put these in when you want to debug cases of revert ------------- + // console.log("------------------------------------"); + // console.log("Addresses:"); + // console.log(`admin = ${admin.address}`); + // console.log(`user1 = ${user1.address}`); + // console.log(`user2 = ${user2.address}`); + // console.log(`user3 = ${user3.address}`); + // console.log(`talentToken = ${talentToken.address}`); + // console.log(`talentVault = ${talentVault.address}`); + // console.log("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); // Approve TalentVault contract to spend tokens on behalf of the admin const totalAllowance = ethers.utils.parseUnits("600000000", 18); @@ -69,8 +67,6 @@ describe("TalentVault", () => { await talentToken.transfer(yieldSource.address, ethers.utils.parseEther("100000")); await talentToken.connect(yieldSource).approve(talentVault.address, ethers.utils.parseEther("100000")); - // await talentToken.renounceOwnership(); - snapshotId = await ethers.provider.send("evm_snapshot", []); }); @@ -636,11 +632,9 @@ describe("TalentVault", () => { await talentVault.connect(user1).deposit(depositAmount, user1.address); const talentVaultTalentBalanceBefore = await talentToken.balanceOf(talentVault.address); - console.log("talentVaultTalentBalanceBefore", ethers.utils.formatEther(talentVaultTalentBalanceBefore)); const yieldSourceTalentBalanceBefore = await talentToken.balanceOf(yieldSource.address); const user1TalentVaultBalanceBefore = await talentVault.balanceOf(user1.address); - console.log("user1TalentVaultBalanceBefore", ethers.utils.formatEther(user1TalentVaultBalanceBefore)); expect(user1TalentVaultBalanceBefore).to.equal(depositAmount); // Simulate time passing @@ -699,13 +693,10 @@ describe("TalentVault", () => { await talentVault.connect(user1).refresh(); const userBalance = await talentVault.balanceOf(user1.address); - console.log("userBalance", ethers.utils.formatEther(userBalance)); expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.001")); const userLastInterestCalculation = (await talentVault.userBalanceMeta(user1.address)).lastInterestCalculation; - console.log("userLastInterestCalculation", userLastInterestCalculation); const currentDateEpochSeconds = Math.floor(Date.now() / 1000); - console.log("currentDateEpochSeconds", currentDateEpochSeconds); const oneYearAfterEpochSeconds = currentDateEpochSeconds + 31536000; expect(userLastInterestCalculation.toNumber()).to.be.closeTo(oneYearAfterEpochSeconds, 500); From 703f29781ca1e87f66537d0d9ff9e16d897efcaf Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 10:22:19 +0200 Subject: [PATCH 40/74] [spec] Deploy contracts only once, at the beginning of the test suite run --- test/contracts/talent/TalentVault.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index f03edea2..c04adb00 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -25,7 +25,7 @@ describe("TalentVault", () => { let snapshotId: bigint; - beforeEach(async () => { + before(async () => { [admin, yieldSource, user1, user2, user3] = await ethers.getSigners(); talentToken = (await deployContract(admin, Artifacts.TalentProtocolToken, [admin.address])) as TalentProtocolToken; @@ -43,16 +43,15 @@ describe("TalentVault", () => { passportBuilderScore.address, ])) as TalentVault; - // ------------- put these in when you want to debug cases of revert ------------- - // console.log("------------------------------------"); - // console.log("Addresses:"); - // console.log(`admin = ${admin.address}`); - // console.log(`user1 = ${user1.address}`); - // console.log(`user2 = ${user2.address}`); - // console.log(`user3 = ${user3.address}`); - // console.log(`talentToken = ${talentToken.address}`); - // console.log(`talentVault = ${talentVault.address}`); - // console.log("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); + console.log("------------------------------------"); + console.log("Addresses:"); + console.log(`admin = ${admin.address}`); + console.log(`user1 = ${user1.address}`); + console.log(`user2 = ${user2.address}`); + console.log(`user3 = ${user3.address}`); + console.log(`talentToken = ${talentToken.address}`); + console.log(`talentVault = ${talentVault.address}`); + console.log("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); // Approve TalentVault contract to spend tokens on behalf of the admin const totalAllowance = ethers.utils.parseUnits("600000000", 18); @@ -66,7 +65,9 @@ describe("TalentVault", () => { // fund the yieldSource with lots of TALENT Balance await talentToken.transfer(yieldSource.address, ethers.utils.parseEther("100000")); await talentToken.connect(yieldSource).approve(talentVault.address, ethers.utils.parseEther("100000")); + }); + beforeEach(async () => { snapshotId = await ethers.provider.send("evm_snapshot", []); }); From 8c1e22ddde534327500ca41abee80b622729762d Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 10:25:35 +0200 Subject: [PATCH 41/74] [spec] Added yieldSource address print at the beginning --- test/contracts/talent/TalentVault.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index c04adb00..bcb79632 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -51,6 +51,7 @@ describe("TalentVault", () => { console.log(`user3 = ${user3.address}`); console.log(`talentToken = ${talentToken.address}`); console.log(`talentVault = ${talentVault.address}`); + console.log(`yieldSource = ${yieldSource.address}`); console.log("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); // Approve TalentVault contract to spend tokens on behalf of the admin From 5bd0e8ff0837496431988644b6d8fa51d7056637 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 10:28:11 +0200 Subject: [PATCH 42/74] [spec] Removed comments not needed --- test/contracts/talent/TalentVault.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index bcb79632..d32519f5 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -127,8 +127,6 @@ describe("TalentVault", () => { }); }); - // TODO: StopYieldingInterest - describe("#name", async () => { it("is 'TalentProtocolVaultToken' reflects the underlying token name, i.e. of 'TalentProtocolToken'", async () => { const name = await talentVault.name(); From 64521e2d47e437cdb8129f06f558b092627794b3 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 11:55:04 +0200 Subject: [PATCH 43/74] Removed yieldXXXX state properties which were not used --- contracts/talent/TalentVault.sol | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 62024afb..d1ca35c4 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -63,18 +63,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% uint256 public yieldRateBase; - /// @notice The yield rate for the contract for competent builders, represented as a percentage. - /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% - uint256 public yieldRateCompetent; - - /// @notice The yield rate for the contract for proficient builders, represented as a percentage. - /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% - uint256 public yieldRateProficient; - - /// @notice The yield rate for the contract for expert builders, represented as a percentage. - /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% - uint256 public yieldRateExpert; - /// @notice The maximum amount of tokens that can be used to calculate interest. uint256 public maxYieldAmount; From 83c8dee33f514c3f035beb77da9b6edbb87e2b43 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 12:13:41 +0200 Subject: [PATCH 44/74] [spec] - Add arguments to revertedWith and then fix a test --- contracts/talent/TalentVault.sol | 6 ++---- test/contracts/talent/TalentVault.ts | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index d1ca35c4..ce56710b 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -17,10 +17,8 @@ error NoDepositFound(); error TalentVaultNonTransferable(); error TransferFailed(); -/// Based on WLDVault.sol from Worldcoin -/// ref: https://optimistic.etherscan.io/address/0x21c4928109acb0659a88ae5329b5374a3024694c#code -/// @title Talent Vault Contract -/// @author Francisco Leal +/// @title Talent Protocol Vault Token Contract +/// @author Talent Protocol - Francisco Leal, Panagiotis Matsinopoulos /// @notice Allows any $TALENT holders to deposit their tokens and earn interest. contract TalentVault is ERC4626, Ownable, ReentrancyGuard { using SafeERC20 for IERC20; diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index d32519f5..c0b4b441 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -196,7 +196,7 @@ describe("TalentVault", () => { context("when called by a non-owner", async () => { it("reverts", async () => { await expect(talentVault.connect(user1).setMaxDeposit(user2.address, 10n)).to.revertedWith( - "OwnableUnauthorizedAccount" + `OwnableUnauthorizedAccount("${user1.address}")` ); }); }); @@ -216,7 +216,7 @@ describe("TalentVault", () => { context("when called by a non-owner", async () => { it("reverts", async () => { await expect(talentVault.connect(user1).removeMaxDepositLimit(user2.address)).to.revertedWith( - "OwnableUnauthorizedAccount" + `OwnableUnauthorizedAccount("${user1.address}")` ); }); }); @@ -320,7 +320,7 @@ describe("TalentVault", () => { await talentToken.connect(user1).approve(talentVault.address, depositAmount); await expect(talentVault.connect(user1).deposit(depositAmount, user2.address)).to.be.revertedWith( - "ERC20InsufficientBalance" + `ERC20InsufficientBalance("${user1.address}", ${balanceOfUser1}, ${depositAmount})` ); }); @@ -336,18 +336,20 @@ describe("TalentVault", () => { // fire await expect(talentVault.connect(user1).deposit(depositAmount, user2.address)).to.be.revertedWith( - "ERC20InsufficientAllowance" + `ERC20InsufficientAllowance("${talentVault.address}", ${approvedAmount}, ${depositAmount})` ); }); it("Should allow deposit of amount equal to the allowed by the sender to be spent by the talent contract", async () => { const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.transfer(user1.address, depositAmount); + await talentToken.connect(user1).approve(talentVault.address, depositAmount); - await expect(talentVault.connect(user1).deposit(depositAmount, user2.address)).to.be.revertedWith( - "ERC20InsufficientBalance" - ); + // fire + + await expect(talentVault.connect(user1).deposit(depositAmount, user2.address)).not.to.be.reverted; }); }); @@ -365,7 +367,7 @@ describe("TalentVault", () => { context("when called by a non-owner", async () => { it("reverts", async () => { await expect(talentVault.connect(user1).setMaxMint(user2.address, 10n)).to.revertedWith( - "OwnableUnauthorizedAccount" + `OwnableUnauthorizedAccount("${user1.address}")` ); }); }); @@ -385,7 +387,7 @@ describe("TalentVault", () => { context("when called by a non-owner", async () => { it("reverts", async () => { await expect(talentVault.connect(user1).removeMaxMintLimit(user2.address)).to.revertedWith( - "OwnableUnauthorizedAccount" + `OwnableUnauthorizedAccount("${user1.address}")` ); }); }); From a319903afae742b67affa2fd5b8beb837e0709a4 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 12:38:57 +0200 Subject: [PATCH 45/74] [spec] Better handling of block.timestamp for testing --- test/contracts/talent/TalentVault.ts | 34 ++++++++++++---------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index c0b4b441..7ae7d667 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -5,6 +5,7 @@ import { solidity } from "ethereum-waffle"; import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { TalentProtocolToken, TalentVault, PassportRegistry, PassportBuilderScore } from "../../../typechain-types"; import { Artifacts } from "../../shared"; +import { ensureTimestamp } from "../../shared/utils"; chai.use(solidity); @@ -24,6 +25,7 @@ describe("TalentVault", () => { let talentVault: TalentVault; let snapshotId: bigint; + let currentDateEpochSeconds: number; before(async () => { [admin, yieldSource, user1, user2, user3] = await ethers.getSigners(); @@ -70,6 +72,7 @@ describe("TalentVault", () => { beforeEach(async () => { snapshotId = await ethers.provider.send("evm_snapshot", []); + currentDateEpochSeconds = Math.floor(Date.now() / 1000); }); afterEach(async () => { @@ -640,14 +643,12 @@ describe("TalentVault", () => { expect(user1TalentVaultBalanceBefore).to.equal(depositAmount); // Simulate time passing - await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - await ethers.provider.send("evm_mine", []); + ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead const yieldedInterest = depositAmount.mul(10).div(100); // 10% interest // this is manually calculated, but it is necessary for this test. const expectedUser1TalentVaultBalanceAfter1Year = ethers.utils.parseEther("1100.000003170979198376"); - // const expectedUserTalentVaultBalanceAfter1Year = depositAmount; // fire await talentVault.connect(user1).withdrawAll(); @@ -686,8 +687,7 @@ describe("TalentVault", () => { await talentVault.connect(user1).deposit(depositAmount, user1.address); // Simulate time passing - await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - await ethers.provider.send("evm_mine", []); + ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead const expectedInterest = depositAmount.mul(10).div(100); // 10% interest @@ -698,10 +698,9 @@ describe("TalentVault", () => { expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.001")); const userLastInterestCalculation = (await talentVault.userBalanceMeta(user1.address)).lastInterestCalculation; - const currentDateEpochSeconds = Math.floor(Date.now() / 1000); const oneYearAfterEpochSeconds = currentDateEpochSeconds + 31536000; - expect(userLastInterestCalculation.toNumber()).to.be.closeTo(oneYearAfterEpochSeconds, 500); + expect(userLastInterestCalculation.toNumber()).to.equal(oneYearAfterEpochSeconds); }); context("when yielding interest is stopped", async () => { @@ -712,11 +711,11 @@ describe("TalentVault", () => { await talentToken.connect(user1).approve(talentVault.address, depositAmount); await talentVault.connect(user1).deposit(depositAmount, user1.address); + await talentVault.stopYieldingInterest(); + // Simulate time passing - await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - await ethers.provider.send("evm_mine", []); - await talentVault.stopYieldingInterest(); + ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead // fire await talentVault.connect(user1).refresh(); @@ -725,10 +724,9 @@ describe("TalentVault", () => { expect(user1BalanceAfter).to.equal(user1BalanceBefore); const userLastInterestCalculation = (await talentVault.userBalanceMeta(user1.address)).lastInterestCalculation; - const currentDateEpochSeconds = Math.floor(Date.now() / 1000); const oneYearAfterEpochSeconds = currentDateEpochSeconds + 31536000; - expect(userLastInterestCalculation.toNumber()).to.be.closeTo(oneYearAfterEpochSeconds, 500); + expect(userLastInterestCalculation.toNumber()).to.equal(oneYearAfterEpochSeconds); }); }); @@ -741,8 +739,7 @@ describe("TalentVault", () => { await talentVault.connect(user1).deposit(depositAmount, user1.address); // Simulate time passing - await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - await ethers.provider.send("evm_mine", []); + ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead const expectedInterest = maxAmount.mul(10).div(100); // 10% interest @@ -765,8 +762,7 @@ describe("TalentVault", () => { await talentVault.connect(user1).deposit(depositAmount, user1.address); // Simulate time passing - await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - await ethers.provider.send("evm_mine", []); + ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 @@ -790,8 +786,7 @@ describe("TalentVault", () => { await talentVault.connect(user1).deposit(depositAmount, user1.address); // Simulate time passing - await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - await ethers.provider.send("evm_mine", []); + ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 @@ -815,8 +810,7 @@ describe("TalentVault", () => { await talentVault.connect(user1).deposit(depositAmount, user1.address); // Simulate time passing - await ethers.provider.send("evm_increaseTime", [31536000]); // 1 year - await ethers.provider.send("evm_mine", []); + ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 From cbe70fbbbee5092fd754f9c61fc0583607feaefe Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 12:56:25 +0200 Subject: [PATCH 46/74] [spec] hardhat reset at the beginning of TalentVault suite --- test/contracts/talent/TalentVault.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 7ae7d667..1a473abc 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -28,6 +28,8 @@ describe("TalentVault", () => { let currentDateEpochSeconds: number; before(async () => { + await ethers.provider.send("hardhat_reset", []); + [admin, yieldSource, user1, user2, user3] = await ethers.getSigners(); talentToken = (await deployContract(admin, Artifacts.TalentProtocolToken, [admin.address])) as TalentProtocolToken; From fb19b5de88d368dd5fcc7d45c3ee1c4023a243b1 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 13:06:38 +0200 Subject: [PATCH 47/74] WIP failing tests --- contracts/talent/TalentVault.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index ce56710b..1131ac8e 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -44,12 +44,12 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { } /// @notice The number of seconds in a year - uint256 public constant SECONDS_PER_YEAR = 31536000; + uint256 internal constant SECONDS_PER_YEAR = 31536000; /// @notice The maximum yield rate that can be set, represented as a percentage. - uint256 public constant ONE_HUNDRED_PERCENT = 100_00; + uint256 internal constant ONE_HUNDRED_PERCENT = 100_00; - uint256 public constant SECONDS_PER_YEAR_x_ONE_HUNDRED_PERCENT = SECONDS_PER_YEAR * ONE_HUNDRED_PERCENT; + uint256 internal constant SECONDS_PER_YEAR_x_ONE_HUNDRED_PERCENT = SECONDS_PER_YEAR * ONE_HUNDRED_PERCENT; /// @notice The token that will be deposited into the contract IERC20 public immutable token; @@ -109,21 +109,21 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { passportBuilderScore = _passportBuilderScore; } - function setMaxDeposit(address receiver, uint256 assets) public onlyOwner { + function setMaxDeposit(address receiver, uint256 assets) internal onlyOwner { maxDeposits[receiver] = assets; maxDepositLimitFlags[receiver] = true; } - function setMaxMint(address receiver, uint256 shares) public onlyOwner { + function setMaxMint(address receiver, uint256 shares) external onlyOwner { setMaxDeposit(receiver, shares); } - function removeMaxDepositLimit(address receiver) public onlyOwner { + function removeMaxDepositLimit(address receiver) internal onlyOwner { delete maxDeposits[receiver]; delete maxDepositLimitFlags[receiver]; } - function removeMaxMintLimit(address receiver) public onlyOwner { + function removeMaxMintLimit(address receiver) external onlyOwner { removeMaxDepositLimit(receiver); } From cb8e367d9e881e6db6dd90fbe473e993fe44bb68 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Thu, 31 Oct 2024 11:24:18 +0000 Subject: [PATCH 48/74] Improve Talent Reward Claim --- contracts/talent/TalentRewardClaim.sol | 1 + scripts/passport/migrateMainWallets.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/talent/TalentRewardClaim.sol b/contracts/talent/TalentRewardClaim.sol index e3731bcd..f81bd033 100644 --- a/contracts/talent/TalentRewardClaim.sol +++ b/contracts/talent/TalentRewardClaim.sol @@ -84,6 +84,7 @@ contract TalentRewardClaim is Ownable, ReentrancyGuard { uint256 amountAllocated ) external nonReentrant { require(startTime > 0, "Start time not set"); + require(block.timestamp >= startTime, "Claiming has not started yet"); verify(merkleProof, amountAllocated); diff --git a/scripts/passport/migrateMainWallets.ts b/scripts/passport/migrateMainWallets.ts index f033ed60..3502fb63 100644 --- a/scripts/passport/migrateMainWallets.ts +++ b/scripts/passport/migrateMainWallets.ts @@ -2,11 +2,13 @@ import { ethers, network } from "hardhat"; import { PassportWalletRegistry } from "../../test/shared/artifacts"; import MAIN_WALLET_CHANGES from "../data/main-wallet-changes.json"; +const PASSPORT_WALLET_REGISTRY_MAINNET = "0x9B729d9fC43e3746855F7E02238FB3a2A20bD899"; + async function main() { const [admin] = await ethers.getSigners(); const passportWalletRegistry = new ethers.Contract( - "0x9B729d9fC43e3746855F7E02238FB3a2A20bD899", + PASSPORT_WALLET_REGISTRY_MAINNET, PassportWalletRegistry.abi, admin ); From b501df4b74e0cc59a63e7cb9d8280249b03263b4 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Thu, 31 Oct 2024 11:28:56 +0000 Subject: [PATCH 49/74] Remove tests of internal functions --- test/contracts/talent/TalentVault.ts | 50 ---------------------------- 1 file changed, 50 deletions(-) diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 1a473abc..ee9331bf 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -187,46 +187,6 @@ describe("TalentVault", () => { }); }); - describe("#setMaxDeposit", async () => { - context("when called by the owner", async () => { - it("sets the maximum deposit for the receiver", async () => { - await talentVault.setMaxDeposit(user1.address, 10n); - - const deposit = await talentVault.maxDeposit(user1.address); - - expect(deposit).to.equal(10n); - }); - }); - - context("when called by a non-owner", async () => { - it("reverts", async () => { - await expect(talentVault.connect(user1).setMaxDeposit(user2.address, 10n)).to.revertedWith( - `OwnableUnauthorizedAccount("${user1.address}")` - ); - }); - }); - }); - - describe("#removeMaxDepositLimit", async () => { - context("when called by the owner", async () => { - it("removes the maximum deposit for the receiver", async () => { - await talentVault.removeMaxDepositLimit(user1.address); - - const deposit = await talentVault.maxDeposit(user1.address); - - expect(deposit).to.equal(ethers.constants.MaxUint256); - }); - }); - - context("when called by a non-owner", async () => { - it("reverts", async () => { - await expect(talentVault.connect(user1).removeMaxDepositLimit(user2.address)).to.revertedWith( - `OwnableUnauthorizedAccount("${user1.address}")` - ); - }); - }); - }); - describe("#maxDeposit", async () => { context("when recipient does not have a deposit limit", async () => { it("returns the maximum uint256", async () => { @@ -235,16 +195,6 @@ describe("TalentVault", () => { expect(maxDeposit).to.equal(ethers.constants.MaxUint256); }); }); - - context("when recipient has a deposit limit", async () => { - it("returns it", async () => { - await talentVault.setMaxDeposit(user1.address, 5n); - - const maxDeposit = await talentVault.maxDeposit(user1.address); - - expect(maxDeposit).to.equal(5n); - }); - }); }); describe("#convertToShares", async () => { From 18a0c10c0e2779b42993cc4998734da30c2fc998 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Thu, 31 Oct 2024 11:31:47 +0000 Subject: [PATCH 50/74] Add setYieldSource function --- contracts/talent/TalentVault.sol | 4 ++++ test/contracts/talent/TalentVault.ts | 36 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 1131ac8e..faa3cd39 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -259,6 +259,10 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { yieldInterestFlag = true; } + function setYieldSource(address _yieldSource) external onlyOwner { + yieldSource = _yieldSource; + } + // ---------- INTERNAL -------------------------------------- function calculateInterest(address user) internal returns (uint256) { diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index ee9331bf..a694c04c 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -348,6 +348,26 @@ describe("TalentVault", () => { }); }); + describe("#setYieldSource", async () => { + context("when called by the owner", async () => { + it("sets the yield source", async () => { + await talentVault.setYieldSource(user1.address); + + const yieldSource = await talentVault.yieldSource(); + + expect(yieldSource).to.equal(user1.address); + }); + }); + + context("when called by a non-owner", async () => { + it("reverts", async () => { + await expect(talentVault.connect(user1).setYieldSource(user2.address)).to.revertedWith( + `OwnableUnauthorizedAccount("${user1.address}")` + ); + }); + }); + }); + describe("#maxMint", async () => { context("when recipient does not have a mint limit", async () => { it("returns the maximum uint256", async () => { @@ -461,8 +481,16 @@ describe("TalentVault", () => { trx = await talentVault.connect(user1).withdraw(depositTalent, user1.address, user1.address); const receipt = await trx.wait(); + if (!receipt.events) { + throw new Error("No events found"); + } + const withdrawEvent = receipt.events.find((event) => event.event === "Withdraw"); + if (!withdrawEvent || !withdrawEvent.args) { + throw new Error("Withdraw event not found"); + } + const talentVaultWithDrawn = withdrawEvent.args[4]; expect(talentVaultWithDrawn).to.equal(depositTalent); @@ -523,8 +551,16 @@ describe("TalentVault", () => { trx = await talentVault.connect(user1).redeem(equivalentDepositTalentVault, user1.address, user1.address); const receipt = await trx.wait(); + if (!receipt.events) { + throw new Error("No events found"); + } + const withdrawEvent = receipt.events.find((event) => event.event === "Withdraw"); + if (!withdrawEvent || !withdrawEvent.args) { + throw new Error("Withdraw event not found"); + } + const talentWithDrawn = withdrawEvent.args[4]; expect(talentWithDrawn).to.equal(equivalentDepositTalentVault); From c0d958de54d2c73bf4589c79bb752c1a0d8e988d Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Thu, 31 Oct 2024 11:39:55 +0000 Subject: [PATCH 51/74] Update calculating interest to be a view function --- contracts/talent/TalentVault.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index faa3cd39..62f9b773 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -265,12 +265,10 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { // ---------- INTERNAL -------------------------------------- - function calculateInterest(address user) internal returns (uint256) { + function calculateInterest(address user) public view returns (uint256) { UserBalanceMeta storage balanceMeta = userBalanceMeta[user]; if (!yieldInterestFlag) { - balanceMeta.lastInterestCalculation = block.timestamp; - return 0; } @@ -300,14 +298,15 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { uint256 yieldRate = getYieldRateForScore(user); - balanceMeta.lastInterestCalculation = block.timestamp; - return (userBalance * yieldRate * timeElapsed) / (SECONDS_PER_YEAR_x_ONE_HUNDRED_PERCENT); } /// @dev Refreshes the balance of an address function yieldInterest(address user) internal { + UserBalanceMeta storage balanceMeta = userBalanceMeta[user]; uint256 interest = calculateInterest(user); + balanceMeta.lastInterestCalculation = block.timestamp; + _deposit(yieldSource, user, interest, interest); } From 23bcfe19afe0b7b5f83fbb25058927666867681e Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Thu, 31 Oct 2024 12:46:04 +0000 Subject: [PATCH 52/74] Add deploy TalentVault script --- scripts/shared/index.ts | 10 +++++++++ scripts/talent/deployTalentVault.ts | 33 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 scripts/talent/deployTalentVault.ts diff --git a/scripts/shared/index.ts b/scripts/shared/index.ts index 5774889c..35caca93 100644 --- a/scripts/shared/index.ts +++ b/scripts/shared/index.ts @@ -10,6 +10,7 @@ import type { SmartBuilderScore, PassportWalletRegistry, TalentTGEUnlockTimestamp, + TalentVault, } from "../../typechain-types"; import { BigNumber } from "ethers"; import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; @@ -137,3 +138,12 @@ export async function deployTalentTGEUnlockTimestamps( await deployedTGEUnlock.deployed(); return deployedTGEUnlock as TalentTGEUnlockTimestamp; } + +export async function deployTalentVault(owner: string, talentToken: string, yieldSource: string): Promise { + const talentVaultContract = await ethers.getContractFactory("TalentVault"); + + const deployedTalentVault = await talentVaultContract.deploy(owner, talentToken, yieldSource); + await deployedTalentVault.deployed(); + + return deployedTalentVault as TalentVault; +} diff --git a/scripts/talent/deployTalentVault.ts b/scripts/talent/deployTalentVault.ts new file mode 100644 index 00000000..ceba9604 --- /dev/null +++ b/scripts/talent/deployTalentVault.ts @@ -0,0 +1,33 @@ +import { ethers, network } from "hardhat"; +import { BigNumber } from "ethers"; +import { deployTalentVault } from "../shared"; + +const TALENT_TOKEN_MAINNET = ""; +const TALENT_TOKEN_TESTNET = ""; + +const YIELD_SOURCE_MAINNET = ""; +const YIELD_SOURCE_TESTNET = ""; + +async function main() { + console.log(`Deploying Talent Vault at ${network.name}`); + + const [admin] = await ethers.getSigners(); + + console.log(`Admin will be ${admin.address}`); + + const talentVault = await deployTalentVault(admin.address, TALENT_TOKEN_MAINNET, YIELD_SOURCE_MAINNET); + + console.log(`Talent Vault deployed at ${talentVault.address}`); + console.log( + `Params for verification: Contract ${talentVault.address} Owner ${admin.address} Talent Token ${TALENT_TOKEN_MAINNET} Yield Source ${YIELD_SOURCE_MAINNET}` + ); + + console.log("Done"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); From 020401fed653e1034a20d022804f473a1bbde007 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 17:39:14 +0200 Subject: [PATCH 53/74] Solc version pinned for asdf --- .tool-versions | 1 + 1 file changed, 1 insertion(+) diff --git a/.tool-versions b/.tool-versions index 958fb369..c6743f9b 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1,2 @@ nodejs 20.12.2 +solidity 0.8.24 From 24efcf061b9298e8d4c909d6f4e7c2dd212c5fac Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 17:39:50 +0200 Subject: [PATCH 54/74] Minor: Reorganization of the function definitions, first external, then public, then internal --- contracts/talent/TalentVault.sol | 156 +++++++++++++++---------------- 1 file changed, 77 insertions(+), 79 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 62f9b773..1164e394 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -8,18 +8,10 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "../passport/PassportBuilderScore.sol"; -error ContractInsolvent(); -error InsufficientAllowance(); -error InsufficientBalance(); -error InvalidAddress(); -error InvalidDepositAmount(); -error NoDepositFound(); -error TalentVaultNonTransferable(); -error TransferFailed(); - /// @title Talent Protocol Vault Token Contract /// @author Talent Protocol - Francisco Leal, Panagiotis Matsinopoulos /// @notice Allows any $TALENT holders to deposit their tokens and earn interest. +/// @dev This is an ERC4626 compliant contract. contract TalentVault is ERC4626, Ownable, ReentrancyGuard { using SafeERC20 for IERC20; @@ -35,9 +27,18 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @param yieldAccrualDeadline The new yield accrual deadline event YieldAccrualDeadlineUpdated(uint256 yieldAccrualDeadline); - /// @notice Represents a user's deposit + error ContractInsolvent(); + error InsufficientAllowance(); + error InsufficientBalance(); + error InvalidAddress(); + error InvalidDepositAmount(); + error NoDepositFound(); + error TalentVaultNonTransferable(); + error TransferFailed(); + + /// @notice Represents user's balance meta data /// @param depositedAmount The amount of tokens that were deposited, excluding interest - /// @param lastInterestCalculation The timestamp of the last interest calculation for this deposit + /// @param lastInterestCalculation The timestamp (seconds since Epoch) of the last interest calculation for this deposit struct UserBalanceMeta { uint256 depositedAmount; uint256 lastInterestCalculation; @@ -109,24 +110,68 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { passportBuilderScore = _passportBuilderScore; } - function setMaxDeposit(address receiver, uint256 assets) internal onlyOwner { - maxDeposits[receiver] = assets; - maxDepositLimitFlags[receiver] = true; - } + // ------------------- EXTERNAL -------------------------------------------- function setMaxMint(address receiver, uint256 shares) external onlyOwner { setMaxDeposit(receiver, shares); } - function removeMaxDepositLimit(address receiver) internal onlyOwner { - delete maxDeposits[receiver]; - delete maxDepositLimitFlags[receiver]; - } - function removeMaxMintLimit(address receiver) external onlyOwner { removeMaxDepositLimit(receiver); } + /// @notice Calculate any accrued interest. + function refresh() external { + refreshForAddress(msg.sender); + } + + /// @notice Withdraws all of the user's balance, including any accrued interest. + function withdrawAll() external nonReentrant { + refreshForAddress(msg.sender); + redeem(balanceOf(msg.sender), msg.sender, msg.sender); + } + + /// @notice Update the yield rate for the contract + /// @dev Can only be called by the owner + function setYieldRate(uint256 _yieldRate) external onlyOwner { + require(_yieldRate > yieldRateBase, "Yield rate cannot be decreased"); + + yieldRateBase = _yieldRate; + emit YieldRateUpdated(_yieldRate); + } + + /// @notice Update the maximum amount of tokens that can be used to calculate interest + /// @dev Can only be called by the owner + function setMaxYieldAmount(uint256 _maxYieldAmount) external onlyOwner { + maxYieldAmount = _maxYieldAmount; + + emit MaxYieldAmountUpdated(_maxYieldAmount); + } + + /// @notice Update the time at which the users of the contract will stop accruing interest + /// @dev Can only be called by the owner + function setYieldAccrualDeadline(uint256 _yieldAccrualDeadline) external onlyOwner { + require(_yieldAccrualDeadline > block.timestamp, "Invalid yield accrual deadline"); + + yieldAccrualDeadline = _yieldAccrualDeadline; + + emit YieldAccrualDeadlineUpdated(_yieldAccrualDeadline); + } + + function stopYieldingInterest() external onlyOwner { + yieldInterestFlag = false; + } + + function startYieldingInterest() external onlyOwner { + yieldInterestFlag = true; + } + + function setYieldSource(address _yieldSource) external onlyOwner { + yieldSource = _yieldSource; + } + + // ------------------------- PUBLIC ---------------------------------------------------- + function maxDeposit(address receiver) public view virtual override returns (uint256) { if (maxDepositLimitFlags[receiver]) { return maxDeposits[receiver]; @@ -150,12 +195,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { UserBalanceMeta storage balanceMeta = userBalanceMeta[receiver]; - // This we don't need it only for reporting purposes. - // Note that the +assets+ is $TALENT that is moved from - // +receiver+ wallet to +address(this)+ wallet. - // But this one here, it gives us an easy way to find out - // how much $TALENT a +receiveruserBalanceMetadeposited. - balanceMeta.depositedAmount += assets; return shares; @@ -177,26 +216,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { yieldInterest(account); } - /// @notice Calculate any accrued interest. - function refresh() external { - refreshForAddress(msg.sender); - } - - /// @notice Withdraws all of the user's balance, including any accrued interest. - function withdrawAll() external nonReentrant { - refreshForAddress(msg.sender); - redeem(balanceOf(msg.sender), msg.sender, msg.sender); - } - - /// @notice Update the yield rate for the contract - /// @dev Can only be called by the owner - function setYieldRate(uint256 _yieldRate) external onlyOwner { - require(_yieldRate > yieldRateBase, "Yield rate cannot be decreased"); - - yieldRateBase = _yieldRate; - emit YieldRateUpdated(_yieldRate); - } - /// @notice Get the yield rate for the contract for a given user /// @param user The address of the user to get the yield rate for function getYieldRateForScore(address user) public view returns (uint256) { @@ -209,24 +228,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { return yieldRateBase + 15_00; } - /// @notice Update the maximum amount of tokens that can be used to calculate interest - /// @dev Can only be called by the owner - function setMaxYieldAmount(uint256 _maxYieldAmount) external onlyOwner { - maxYieldAmount = _maxYieldAmount; - - emit MaxYieldAmountUpdated(_maxYieldAmount); - } - - /// @notice Update the time at which the users of the contract will stop accruing interest - /// @dev Can only be called by the owner - function setYieldAccrualDeadline(uint256 _yieldAccrualDeadline) external onlyOwner { - require(_yieldAccrualDeadline > block.timestamp, "Invalid yield accrual deadline"); - - yieldAccrualDeadline = _yieldAccrualDeadline; - - emit YieldAccrualDeadlineUpdated(_yieldAccrualDeadline); - } - /// @notice Prevents the owner from renouncing ownership /// @dev Can only be called by the owner function renounceOwnership() public view override onlyOwner { @@ -251,20 +252,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { revert TalentVaultNonTransferable(); } - function stopYieldingInterest() external onlyOwner { - yieldInterestFlag = false; - } - - function startYieldingInterest() external onlyOwner { - yieldInterestFlag = true; - } - - function setYieldSource(address _yieldSource) external onlyOwner { - yieldSource = _yieldSource; - } - - // ---------- INTERNAL -------------------------------------- - function calculateInterest(address user) public view returns (uint256) { UserBalanceMeta storage balanceMeta = userBalanceMeta[user]; @@ -301,13 +288,24 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { return (userBalance * yieldRate * timeElapsed) / (SECONDS_PER_YEAR_x_ONE_HUNDRED_PERCENT); } + // ---------- INTERNAL -------------------------------------- + + function setMaxDeposit(address receiver, uint256 assets) internal onlyOwner { + maxDeposits[receiver] = assets; + maxDepositLimitFlags[receiver] = true; + } + + function removeMaxDepositLimit(address receiver) internal onlyOwner { + delete maxDeposits[receiver]; + delete maxDepositLimitFlags[receiver]; + } + /// @dev Refreshes the balance of an address function yieldInterest(address user) internal { UserBalanceMeta storage balanceMeta = userBalanceMeta[user]; uint256 interest = calculateInterest(user); balanceMeta.lastInterestCalculation = block.timestamp; - _deposit(yieldSource, user, interest, interest); } } From c72e93ac636c6844c8f2984817f15417231fec01 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 20:32:47 +0200 Subject: [PATCH 55/74] Allow withdraw only after lock period --- contracts/talent/TalentVault.sol | 24 +++++++++++++++++ test/contracts/talent/TalentVault.ts | 39 ++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 1164e394..e34b1f85 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -27,6 +27,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @param yieldAccrualDeadline The new yield accrual deadline event YieldAccrualDeadlineUpdated(uint256 yieldAccrualDeadline); + error CantWithdrawWithinTheLockPeriod(); error ContractInsolvent(); error InsufficientAllowance(); error InsufficientBalance(); @@ -42,8 +43,13 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { struct UserBalanceMeta { uint256 depositedAmount; uint256 lastInterestCalculation; + uint256 lastDepositAt; } + /// @notice The amount of days that your deposits are locked and can't be withdrawn. + /// Lock period end-day is calculated base on the last datetime user did a deposit. + uint256 public constant LOCK_PERIOD = 7 days; + /// @notice The number of seconds in a year uint256 internal constant SECONDS_PER_YEAR = 31536000; @@ -197,6 +203,8 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { balanceMeta.depositedAmount += assets; + balanceMeta.lastDepositAt = block.timestamp; + return shares; } @@ -308,4 +316,20 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { _deposit(yieldSource, user, interest, interest); } + + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares + ) internal virtual override { + UserBalanceMeta storage receiverUserBalanceMeta = userBalanceMeta[receiver]; + + if (receiverUserBalanceMeta.lastDepositAt + LOCK_PERIOD > block.timestamp) { + revert CantWithdrawWithinTheLockPeriod(); + } + + super._withdraw(caller, receiver, owner, assets, shares); + } } diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index a694c04c..4d05d09a 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -12,6 +12,12 @@ chai.use(solidity); const { expect } = chai; const { deployContract } = waffle; +async function ensureTimeIsAfterLockPeriod() { + const lockPeriod = 8; + const oneDayAfterLockPeriod = Math.floor(Date.now() / 1000) + lockPeriod * 24 * 60 * 60; + await ensureTimestamp(oneDayAfterLockPeriod); +} + describe("TalentVault", () => { let admin: SignerWithAddress; let yieldSource: SignerWithAddress; @@ -255,6 +261,7 @@ describe("TalentVault", () => { expect(user2BalanceMetaAfter.depositedAmount).to.equal( user2BalanceMetaBefore.depositedAmount.toBigInt() + depositAmount ); + expect(user2BalanceMetaAfter.lastDepositAt.toNumber()).to.be.closeTo(currentDateEpochSeconds, 20); // user2 $TALENTVAULT balance is increased const user2TalentVaultBalanceAfter = await talentVault.balanceOf(user2.address); @@ -433,6 +440,7 @@ describe("TalentVault", () => { const userBalanceMeta = await talentVault.userBalanceMeta(user1.address); const depositedAmountAfter = userBalanceMeta.depositedAmount; expect(depositedAmountAfter).to.equal(depositedAmountBefore.toBigInt() + equivalentDepositAmountInTalent); + expect(userBalanceMeta.lastDepositAt.toNumber()).to.be.closeTo(currentDateEpochSeconds, 20); }); it("Should revert if $TALENT deposited is 0", async () => { @@ -465,11 +473,25 @@ describe("TalentVault", () => { }); describe("#withdraw", async () => { + context("when last deposit was within the last 7 days", async () => { + it("reverts", async () => { + await talentToken.transfer(user1.address, 10n); + await talentToken.connect(user1).approve(talentVault.address, 10n); + await talentVault.connect(user1).deposit(10n, user1.address); + + // fire + await expect(talentVault.connect(user1).withdraw(10n, user1.address, user1.address)).to.be.revertedWith( + "CantWithdrawWithinTheLockPeriod" + ); + }); + }); + it("burns $TALENTVAULT from owner, increases $TALENT balance of receiver, decreases $TALENT balance of TalentVault", async () => { const depositTalent = 10_000n; await talentToken.transfer(user1.address, depositTalent); await talentToken.connect(user1).approve(talentVault.address, depositTalent); + let trx = await talentVault.connect(user1).deposit(depositTalent, user1.address); await trx.wait(); @@ -477,6 +499,8 @@ describe("TalentVault", () => { const user1TalentBalanceBefore = await talentToken.balanceOf(user1.address); const talentVaultTalentBalanceBefore = await talentToken.balanceOf(talentVault.address); + await ensureTimeIsAfterLockPeriod(); + // fire trx = await talentVault.connect(user1).withdraw(depositTalent, user1.address, user1.address); const receipt = await trx.wait(); @@ -534,6 +558,19 @@ describe("TalentVault", () => { }); describe("#redeem", async () => { + context("when last deposit was within the last 7 days", async () => { + it("reverts", async () => { + await talentToken.transfer(user1.address, 10n); + await talentToken.connect(user1).approve(talentVault.address, 10n); + await talentVault.connect(user1).deposit(10n, user1.address); + + // fire + await expect(talentVault.connect(user1).withdraw(10n, user1.address, user1.address)).to.be.revertedWith( + "CantWithdrawWithinTheLockPeriod" + ); + }); + }); + it("burns $TALENTVAULT from owner, increases $TALENT balance of receiver, decreases $TALENT balance of TalentVault", async () => { const depositTalent = 10_000n; const equivalentDepositTalentVault = depositTalent; @@ -547,6 +584,8 @@ describe("TalentVault", () => { const user1TalentBalanceBefore = await talentToken.balanceOf(user1.address); const talentVaultTalentBalanceBefore = await talentToken.balanceOf(talentVault.address); + await ensureTimeIsAfterLockPeriod(); + // fire trx = await talentVault.connect(user1).redeem(equivalentDepositTalentVault, user1.address, user1.address); const receipt = await trx.wait(); From e419d51625645301ef77e6c22cf4cc1d23f7d94c Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 20:40:41 +0200 Subject: [PATCH 56/74] Set lockPeriod only by owner --- contracts/talent/TalentVault.sol | 11 +++++++++-- test/contracts/talent/TalentVault.ts | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index e34b1f85..69293ca2 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -48,7 +48,9 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice The amount of days that your deposits are locked and can't be withdrawn. /// Lock period end-day is calculated base on the last datetime user did a deposit. - uint256 public constant LOCK_PERIOD = 7 days; + uint256 public lockPeriod; + + uint256 internal constant SECONDS_WITHIN_DAY = 86400; /// @notice The number of seconds in a year uint256 internal constant SECONDS_PER_YEAR = 31536000; @@ -114,10 +116,15 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { yieldInterestFlag = true; maxYieldAmount = _maxYieldAmount; passportBuilderScore = _passportBuilderScore; + lockPeriod = 7 days; } // ------------------- EXTERNAL -------------------------------------------- + function setLockPeriod(uint256 _lockPeriod) external onlyOwner { + lockPeriod = _lockPeriod * SECONDS_WITHIN_DAY; + } + function setMaxMint(address receiver, uint256 shares) external onlyOwner { setMaxDeposit(receiver, shares); } @@ -326,7 +333,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { ) internal virtual override { UserBalanceMeta storage receiverUserBalanceMeta = userBalanceMeta[receiver]; - if (receiverUserBalanceMeta.lastDepositAt + LOCK_PERIOD > block.timestamp) { + if (receiverUserBalanceMeta.lastDepositAt + lockPeriod > block.timestamp) { revert CantWithdrawWithinTheLockPeriod(); } diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 4d05d09a..7779634d 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -102,6 +102,8 @@ describe("TalentVault", () => { expect(await talentVault.passportBuilderScore()).to.equal(passportBuilderScore.address); expect(await talentVault.yieldInterestFlag()).to.equal(true); + + expect(await talentVault.lockPeriod()).to.equal(7 * 24 * 60 * 60); }); it("reverts with InvalidAddress when _token given is 0", async () => { @@ -900,4 +902,22 @@ describe("TalentVault", () => { }); }); }); + + describe("#setLockPeriod", async () => { + context("when called by a non-owner account", async () => { + it("reverts", async () => { + await expect(talentVault.connect(user1).setLockPeriod(3)).to.be.revertedWith( + `OwnableUnauthorizedAccount("${user1.address}")` + ); + }); + }); + + context("when called by the owner account", async () => { + it("sets the lock period as days given", async () => { + await talentVault.setLockPeriod(10); + + expect(await talentVault.lockPeriod()).to.equal(10 * 24 * 60 * 60); + }); + }); + }); }); From d8435406cab0c2253c12ac38fc37541c7b0e66d5 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Thu, 31 Oct 2024 20:42:21 +0200 Subject: [PATCH 57/74] [lint] Fixed a long line comment --- contracts/talent/TalentVault.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 69293ca2..717aa4ff 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -39,7 +39,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice Represents user's balance meta data /// @param depositedAmount The amount of tokens that were deposited, excluding interest - /// @param lastInterestCalculation The timestamp (seconds since Epoch) of the last interest calculation for this deposit + /// @param lastInterestCalculation The timestamp (seconds since Epoch) of the last interest calculation struct UserBalanceMeta { uint256 depositedAmount; uint256 lastInterestCalculation; From e035715e487c1ea8abfc3963c9c7c0576edb1774 Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Fri, 1 Nov 2024 10:51:34 +0200 Subject: [PATCH 58/74] Optimizer runs to 2**32 - 1 --- hardhat.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index f3969064..2ee75b06 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -30,7 +30,7 @@ const config: HardhatUserConfig = { settings: { optimizer: { enabled: true, - runs: 1000, + runs: 4294967295, }, outputSelection: { "*": { From 61761222512f047541c1a4d2e5b87819b7aac64f Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Fri, 1 Nov 2024 11:28:18 +0200 Subject: [PATCH 59/74] Gas reporter to show method signatures --- hardhat.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/hardhat.config.ts b/hardhat.config.ts index 2ee75b06..a6d3d023 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -55,6 +55,7 @@ const config: HardhatUserConfig = { }, gasReporter: { currency: "ETH", + showMethodSig: true, }, etherscan: { // Your API keys for Etherscan From fe096741952f0ac140905a235c56b5d6479b8f7c Mon Sep 17 00:00:00 2001 From: Panos Matsinopoulos Date: Fri, 1 Nov 2024 11:37:35 +0200 Subject: [PATCH 60/74] Minor: Removed a comment that was not relevant any more --- contracts/talent/TalentVault.sol | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 717aa4ff..2d1437e7 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -96,12 +96,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { address _yieldSource, uint256 _maxYieldAmount, PassportBuilderScore _passportBuilderScore - ) - // uint256 _initialOwnerBalance // added this for the needs to test - ERC4626(_token) - ERC20("TalentProtocolVaultToken", "TALENTVAULT") - Ownable(msg.sender) - { + ) ERC4626(_token) ERC20("TalentProtocolVaultToken", "TALENTVAULT") Ownable(msg.sender) { if ( address(_token) == address(0) || address(_yieldSource) == address(0) || From fe4bda4c5a82a7c7261996ff0c60d17e807aadd5 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Mon, 4 Nov 2024 14:31:15 +0000 Subject: [PATCH 61/74] Update deploy scripts for Vault --- scripts/shared/index.ts | 14 ++++++++++++-- scripts/talent/deployTalentVault.ts | 18 ++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/scripts/shared/index.ts b/scripts/shared/index.ts index 35caca93..496711b8 100644 --- a/scripts/shared/index.ts +++ b/scripts/shared/index.ts @@ -139,10 +139,20 @@ export async function deployTalentTGEUnlockTimestamps( return deployedTGEUnlock as TalentTGEUnlockTimestamp; } -export async function deployTalentVault(owner: string, talentToken: string, yieldSource: string): Promise { +export async function deployTalentVault( + talentToken: string, + yieldSource: string, + maxYieldAmount: BigNumber, + passportBuilderScore: string +): Promise { const talentVaultContract = await ethers.getContractFactory("TalentVault"); - const deployedTalentVault = await talentVaultContract.deploy(owner, talentToken, yieldSource); + const deployedTalentVault = await talentVaultContract.deploy( + talentToken, + yieldSource, + maxYieldAmount, + passportBuilderScore + ); await deployedTalentVault.deployed(); return deployedTalentVault as TalentVault; diff --git a/scripts/talent/deployTalentVault.ts b/scripts/talent/deployTalentVault.ts index ceba9604..b4c507a0 100644 --- a/scripts/talent/deployTalentVault.ts +++ b/scripts/talent/deployTalentVault.ts @@ -3,10 +3,13 @@ import { BigNumber } from "ethers"; import { deployTalentVault } from "../shared"; const TALENT_TOKEN_MAINNET = ""; -const TALENT_TOKEN_TESTNET = ""; +const TALENT_TOKEN_TESTNET = "0x7c2a63e1713578d4d704b462C2dee311A59aE304"; const YIELD_SOURCE_MAINNET = ""; -const YIELD_SOURCE_TESTNET = ""; +const YIELD_SOURCE_TESTNET = "0x33041027dd8F4dC82B6e825FB37ADf8f15d44053"; + +const PASSPORT_BUILDER_SCORE_MAINNET = ""; +const PASSPORT_BUILDER_SCORE_TESTNET = "0x5f3aA689C4DCBAe505E6F6c8548DbD9b908bA71d"; async function main() { console.log(`Deploying Talent Vault at ${network.name}`); @@ -15,11 +18,18 @@ async function main() { console.log(`Admin will be ${admin.address}`); - const talentVault = await deployTalentVault(admin.address, TALENT_TOKEN_MAINNET, YIELD_SOURCE_MAINNET); + const maxYieldAmount = ethers.utils.parseEther("10000"); + + const talentVault = await deployTalentVault( + TALENT_TOKEN_TESTNET, + YIELD_SOURCE_TESTNET, + maxYieldAmount, + PASSPORT_BUILDER_SCORE_TESTNET + ); console.log(`Talent Vault deployed at ${talentVault.address}`); console.log( - `Params for verification: Contract ${talentVault.address} Owner ${admin.address} Talent Token ${TALENT_TOKEN_MAINNET} Yield Source ${YIELD_SOURCE_MAINNET}` + `Params for verification: Contract ${talentVault.address} Owner ${admin.address} Talent Token ${TALENT_TOKEN_TESTNET} Yield Source ${YIELD_SOURCE_TESTNET}` ); console.log("Done"); From cfb063721d619b6416dd731b883c9ec9e5162113 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Mon, 4 Nov 2024 17:00:38 +0000 Subject: [PATCH 62/74] Add extra comments to solidity file --- contracts/talent/TalentVault.sol | 64 +++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 2d1437e7..5f3d623a 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -50,6 +50,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// Lock period end-day is calculated base on the last datetime user did a deposit. uint256 public lockPeriod; + /// @notice The number of seconds in a day uint256 internal constant SECONDS_WITHIN_DAY = 86400; /// @notice The number of seconds in a year @@ -58,6 +59,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice The maximum yield rate that can be set, represented as a percentage. uint256 internal constant ONE_HUNDRED_PERCENT = 100_00; + /// @notice The number of seconds in a year multiplied by 100% (to make it easier to calculate interest) uint256 internal constant SECONDS_PER_YEAR_x_ONE_HUNDRED_PERCENT = SECONDS_PER_YEAR * ONE_HUNDRED_PERCENT; /// @notice The token that will be deposited into the contract @@ -76,14 +78,19 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice The time at which the users of the contract will stop accruing interest uint256 public yieldAccrualDeadline; + /// @notice Whether the contract is accruing interest or not bool public yieldInterestFlag; + /// @notice The Passport Builder Score contract PassportBuilderScore public passportBuilderScore; /// @notice A mapping of user addresses to their deposits mapping(address => UserBalanceMeta) public userBalanceMeta; + /// @notice Whether the max deposit limit is enabled for an address or not mapping(address => bool) private maxDepositLimitFlags; + + /// @notice The maximum deposit amount for an address (if there is one) mapping(address => uint256) private maxDeposits; /// @notice Create a new Talent Vault contract @@ -106,7 +113,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { } token = _token; - yieldRateBase = 10_00; + yieldRateBase = 0; yieldSource = _yieldSource; yieldInterestFlag = true; maxYieldAmount = _maxYieldAmount; @@ -116,19 +123,29 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { // ------------------- EXTERNAL -------------------------------------------- + /// @notice Set the lock period for the contract + /// @dev Can only be called by the owner + /// @param _lockPeriod The lock period in days function setLockPeriod(uint256 _lockPeriod) external onlyOwner { lockPeriod = _lockPeriod * SECONDS_WITHIN_DAY; } + /// @notice Set the maximum deposit amount for an address + /// @dev Can only be called by the owner + /// @param receiver The address to set the maximum deposit amount for + /// @param shares The maximum deposit amount function setMaxMint(address receiver, uint256 shares) external onlyOwner { setMaxDeposit(receiver, shares); } + /// @notice Remove the maximum deposit limit for an address + /// @dev Can only be called by the owner + /// @param receiver The address to remove the maximum deposit limit for function removeMaxMintLimit(address receiver) external onlyOwner { removeMaxDepositLimit(receiver); } - /// @notice Calculate any accrued interest. + /// @notice Calculate any accrued interest for the caller function refresh() external { refreshForAddress(msg.sender); } @@ -139,8 +156,9 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { redeem(balanceOf(msg.sender), msg.sender, msg.sender); } - /// @notice Update the yield rate for the contract + /// @notice Update the base yield rate for the contract /// @dev Can only be called by the owner + /// @param _yieldRate The new yield rate function setYieldRate(uint256 _yieldRate) external onlyOwner { require(_yieldRate > yieldRateBase, "Yield rate cannot be decreased"); @@ -150,6 +168,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice Update the maximum amount of tokens that can be used to calculate interest /// @dev Can only be called by the owner + /// @param _maxYieldAmount The new maximum yield amount function setMaxYieldAmount(uint256 _maxYieldAmount) external onlyOwner { maxYieldAmount = _maxYieldAmount; @@ -158,6 +177,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice Update the time at which the users of the contract will stop accruing interest /// @dev Can only be called by the owner + /// @param _yieldAccrualDeadline The new yield accrual deadline function setYieldAccrualDeadline(uint256 _yieldAccrualDeadline) external onlyOwner { require(_yieldAccrualDeadline > block.timestamp, "Invalid yield accrual deadline"); @@ -166,20 +186,29 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { emit YieldAccrualDeadlineUpdated(_yieldAccrualDeadline); } + /// @notice Stop the contract from accruing interest + /// @dev Can only be called by the owner function stopYieldingInterest() external onlyOwner { yieldInterestFlag = false; } + /// @notice Start the contract accruing interest + /// @dev Can only be called by the owner function startYieldingInterest() external onlyOwner { yieldInterestFlag = true; } + /// @notice Set the yield source for the contract + /// @dev Can only be called by the owner + /// @param _yieldSource The new yield source function setYieldSource(address _yieldSource) external onlyOwner { yieldSource = _yieldSource; } // ------------------------- PUBLIC ---------------------------------------------------- + /// @notice Get the maximum deposit amount for an address + /// @param receiver The address to get the maximum deposit amount for function maxDeposit(address receiver) public view virtual override returns (uint256) { if (maxDepositLimitFlags[receiver]) { return maxDeposits[receiver]; @@ -188,10 +217,15 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { } } + /// @notice Get the maximum deposit amount for an address + /// @param receiver The address to get the maximum deposit amount for function maxMint(address receiver) public view virtual override returns (uint256) { return maxDeposit(receiver); } + /// @notice Deposit tokens into the contract + /// @param assets The amount of tokens to deposit + /// @param receiver The address to deposit the tokens for function deposit(uint256 assets, address receiver) public virtual override returns (uint256) { if (assets <= 0) { revert InvalidDepositAmount(); @@ -210,11 +244,14 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { return shares; } + /// @notice Deposit tokens into the contract + /// @param shares The amount of shares to deposit + /// @param receiver The address to deposit the shares for function mint(uint256 shares, address receiver) public virtual override returns (uint256) { return deposit(shares, receiver); } - /// @notice Calculate any accrued interest. + /// @notice Calculate any accrued interest for an address and update the deposit meta data including minting any interest /// @param account The address of the user to refresh function refreshForAddress(address account) public { if (balanceOf(account) <= 0) { @@ -229,6 +266,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice Get the yield rate for the contract for a given user /// @param user The address of the user to get the yield rate for function getYieldRateForScore(address user) public view returns (uint256) { + /// @TODO: Update to use the PassportWalletRegistry instead for calculating the passport id uint256 passportId = passportBuilderScore.passportRegistry().passportId(user); uint256 builderScore = passportBuilderScore.getScore(passportId); @@ -262,6 +300,8 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { revert TalentVaultNonTransferable(); } + /// @notice Calculate the accrued interest for an address + /// @param user The address to calculate the accrued interest for function calculateInterest(address user) public view returns (uint256) { UserBalanceMeta storage balanceMeta = userBalanceMeta[user]; @@ -300,17 +340,25 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { // ---------- INTERNAL -------------------------------------- + /// @notice Set the maximum deposit amount for an address + /// @dev Can only be called by the owner + /// @param receiver The address to set the maximum deposit amount for + /// @param assets The maximum deposit amount function setMaxDeposit(address receiver, uint256 assets) internal onlyOwner { maxDeposits[receiver] = assets; maxDepositLimitFlags[receiver] = true; } + /// @notice Remove the maximum deposit limit for an address + /// @dev Can only be called by the owner + /// @param receiver The address to remove the maximum deposit limit for function removeMaxDepositLimit(address receiver) internal onlyOwner { delete maxDeposits[receiver]; delete maxDepositLimitFlags[receiver]; } - /// @dev Refreshes the balance of an address + /// @notice Calculate the accrued interest for an address and mint any interest + /// @param user The address to calculate the accrued interest for function yieldInterest(address user) internal { UserBalanceMeta storage balanceMeta = userBalanceMeta[user]; uint256 interest = calculateInterest(user); @@ -319,6 +367,12 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { _deposit(yieldSource, user, interest, interest); } + /// @notice Withdraws tokens from the contract + /// @param caller The address of the caller + /// @param receiver The address of the receiver + /// @param owner The address of the owner + /// @param assets The amount of tokens to withdraw + /// @param shares The amount of shares to withdraw function _withdraw( address caller, address receiver, From 596643d378bd62d94e99305e61f2f3c0685e8f0f Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Mon, 4 Nov 2024 17:08:01 +0000 Subject: [PATCH 63/74] Apply lint --- contracts/talent/TalentVault.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 5f3d623a..aee4fde7 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -251,7 +251,8 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { return deposit(shares, receiver); } - /// @notice Calculate any accrued interest for an address and update the deposit meta data including minting any interest + /// @notice Calculate any accrued interest for an address and update + /// the deposit meta data including minting any interest /// @param account The address of the user to refresh function refreshForAddress(address account) public { if (balanceOf(account) <= 0) { From d6fd4011d2d0e720f2a6f02a51d82f7a9c5727ed Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Mon, 4 Nov 2024 17:11:26 +0000 Subject: [PATCH 64/74] Update tests to match new interest rates --- test/contracts/talent/TalentVault.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 7779634d..08b9dd65 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -674,7 +674,7 @@ describe("TalentVault", () => { // Simulate time passing ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - const yieldedInterest = depositAmount.mul(10).div(100); // 10% interest + const yieldedInterest = depositAmount.mul(0).div(100); // 0% interest // this is manually calculated, but it is necessary for this test. const expectedUser1TalentVaultBalanceAfter1Year = ethers.utils.parseEther("1100.000003170979198376"); @@ -718,7 +718,7 @@ describe("TalentVault", () => { // Simulate time passing ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - const expectedInterest = depositAmount.mul(10).div(100); // 10% interest + const expectedInterest = depositAmount.mul(0).div(100); // 0% interest // fire await talentVault.connect(user1).refresh(); @@ -770,7 +770,7 @@ describe("TalentVault", () => { // Simulate time passing ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - const expectedInterest = maxAmount.mul(10).div(100); // 10% interest + const expectedInterest = maxAmount.mul(10).div(100); // 0% interest // fire await talentVault.connect(user1).refresh(); @@ -798,7 +798,7 @@ describe("TalentVault", () => { // fire await talentVault.connect(user1).refresh(); - const expectedInterest = depositAmount.mul(15).div(100); // 15% interest + const expectedInterest = depositAmount.mul(5).div(100); // 5% interest const userBalance = await talentVault.balanceOf(user1.address); expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); }); @@ -822,7 +822,7 @@ describe("TalentVault", () => { // fire await talentVault.connect(user1).refresh(); - const expectedInterest = depositAmount.mul(20).div(100); // 20% interest + const expectedInterest = depositAmount.mul(10).div(100); // 10% interest const userBalance = await talentVault.balanceOf(user1.address); expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); }); @@ -846,7 +846,7 @@ describe("TalentVault", () => { // fire await talentVault.connect(user1).refresh(); - const expectedInterest = depositAmount.mul(25).div(100); // 25% interest + const expectedInterest = depositAmount.mul(15).div(100); // 15% interest const userBalance = await talentVault.balanceOf(user1.address); expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); }); From b71bdc8fba058654e01fcbd3961773c77022d676 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Tue, 5 Nov 2024 16:30:22 +0000 Subject: [PATCH 65/74] Update tests to new reward levels --- contracts/talent/TalentVault.sol | 6 +++--- test/contracts/talent/TalentVault.ts | 24 +++++++++--------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index aee4fde7..120fc1af 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -271,9 +271,9 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { uint256 passportId = passportBuilderScore.passportRegistry().passportId(user); uint256 builderScore = passportBuilderScore.getScore(passportId); - if (builderScore < 25) return yieldRateBase; - if (builderScore < 50) return yieldRateBase + 5_00; - if (builderScore < 75) return yieldRateBase + 10_00; + if (builderScore < 50) return yieldRateBase; + if (builderScore < 75) return yieldRateBase + 5_00; + if (builderScore < 100) return yieldRateBase + 10_00; return yieldRateBase + 15_00; } diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 08b9dd65..5dc9757e 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -94,7 +94,7 @@ describe("TalentVault", () => { }); it("Should set the correct initial values", async () => { - expect(await talentVault.yieldRateBase()).to.equal(10_00); + expect(await talentVault.yieldRateBase()).to.equal(0); expect(await talentVault.maxYieldAmount()).to.equal(ethers.utils.parseEther("10000")); @@ -677,7 +677,7 @@ describe("TalentVault", () => { const yieldedInterest = depositAmount.mul(0).div(100); // 0% interest // this is manually calculated, but it is necessary for this test. - const expectedUser1TalentVaultBalanceAfter1Year = ethers.utils.parseEther("1100.000003170979198376"); + const expectedUser1TalentVaultBalanceAfter1Year = ethers.utils.parseEther("1000"); // there is no interest // fire await talentVault.connect(user1).withdrawAll(); @@ -770,7 +770,7 @@ describe("TalentVault", () => { // Simulate time passing ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - const expectedInterest = maxAmount.mul(10).div(100); // 0% interest + const expectedInterest = maxAmount.mul(0).div(100); // 0% interest // fire await talentVault.connect(user1).refresh(); @@ -779,12 +779,11 @@ describe("TalentVault", () => { expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.001")); }); - it("Should calculate interest correctly for builders with scores below 50", async () => { + it("Should calculate interest correctly for builders with scores above 50 but below 75", async () => { await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode await passportRegistry.connect(user1).create("source1"); const passportId = await passportRegistry.passportId(user1.address); - await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 const depositAmount = ethers.utils.parseEther("1000"); await talentToken.transfer(user1.address, depositAmount); await talentToken.connect(user1).approve(talentVault.address, depositAmount); @@ -792,8 +791,7 @@ describe("TalentVault", () => { // Simulate time passing ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - - await passportBuilderScore.setScore(passportId, 40); // Set builder score below 50 + await passportBuilderScore.setScore(passportId, 55); // Set builder score below 50 // fire await talentVault.connect(user1).refresh(); @@ -803,12 +801,11 @@ describe("TalentVault", () => { expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); }); - it("Should calculate interest correctly for builders with scores above 50", async () => { + it("Should calculate interest correctly for builders with scores above 75 but below 100", async () => { await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode await passportRegistry.connect(user1).create("source1"); const passportId = await passportRegistry.passportId(user1.address); - await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 const depositAmount = ethers.utils.parseEther("1000"); await talentToken.transfer(user1.address, depositAmount); await talentToken.connect(user1).approve(talentVault.address, depositAmount); @@ -816,8 +813,7 @@ describe("TalentVault", () => { // Simulate time passing ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - - await passportBuilderScore.setScore(passportId, 65); // Set builder score above 50 + await passportBuilderScore.setScore(passportId, 80); // Set builder score above 50 // fire await talentVault.connect(user1).refresh(); @@ -827,12 +823,11 @@ describe("TalentVault", () => { expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); }); - it("Should calculate interest correctly for builders with scores above 75", async () => { + it("Should calculate interest correctly for builders with scores above 100", async () => { await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode await passportRegistry.connect(user1).create("source1"); const passportId = await passportRegistry.passportId(user1.address); - await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 const depositAmount = ethers.utils.parseEther("1000"); await talentToken.transfer(user1.address, depositAmount); await talentToken.connect(user1).approve(talentVault.address, depositAmount); @@ -840,8 +835,7 @@ describe("TalentVault", () => { // Simulate time passing ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - - await passportBuilderScore.setScore(passportId, 90); // Set builder score above 75 + await passportBuilderScore.setScore(passportId, 105); // Set builder score above 75 // fire await talentVault.connect(user1).refresh(); From 7599b97bd0ba75e5e1f0a207eca4816b544de28c Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Tue, 5 Nov 2024 16:38:29 +0000 Subject: [PATCH 66/74] Use rewards as a term --- contracts/talent/TalentVault.sol | 76 +++++++++++----------- test/contracts/talent/TalentVault.ts | 95 ++++++++++++++-------------- 2 files changed, 85 insertions(+), 86 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 120fc1af..82229999 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -10,7 +10,7 @@ import "../passport/PassportBuilderScore.sol"; /// @title Talent Protocol Vault Token Contract /// @author Talent Protocol - Francisco Leal, Panagiotis Matsinopoulos -/// @notice Allows any $TALENT holders to deposit their tokens and earn interest. +/// @notice Allows any $TALENT holders to deposit their tokens and earn rewards. /// @dev This is an ERC4626 compliant contract. contract TalentVault is ERC4626, Ownable, ReentrancyGuard { using SafeERC20 for IERC20; @@ -38,11 +38,11 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { error TransferFailed(); /// @notice Represents user's balance meta data - /// @param depositedAmount The amount of tokens that were deposited, excluding interest - /// @param lastInterestCalculation The timestamp (seconds since Epoch) of the last interest calculation + /// @param depositedAmount The amount of tokens that were deposited, excluding rewards + /// @param lastRewardCalculation The timestamp (seconds since Epoch) of the last rewards calculation struct UserBalanceMeta { uint256 depositedAmount; - uint256 lastInterestCalculation; + uint256 lastRewardCalculation; uint256 lastDepositAt; } @@ -59,7 +59,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice The maximum yield rate that can be set, represented as a percentage. uint256 internal constant ONE_HUNDRED_PERCENT = 100_00; - /// @notice The number of seconds in a year multiplied by 100% (to make it easier to calculate interest) + /// @notice The number of seconds in a year multiplied by 100% (to make it easier to calculate rewards) uint256 internal constant SECONDS_PER_YEAR_x_ONE_HUNDRED_PERCENT = SECONDS_PER_YEAR * ONE_HUNDRED_PERCENT; /// @notice The token that will be deposited into the contract @@ -72,14 +72,14 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% uint256 public yieldRateBase; - /// @notice The maximum amount of tokens that can be used to calculate interest. + /// @notice The maximum amount of tokens that can be used to calculate reward. uint256 public maxYieldAmount; - /// @notice The time at which the users of the contract will stop accruing interest + /// @notice The time at which the users of the contract will stop accruing rewards uint256 public yieldAccrualDeadline; - /// @notice Whether the contract is accruing interest or not - bool public yieldInterestFlag; + /// @notice Whether the contract is accruing rewards or not + bool public yieldRewardsFlag; /// @notice The Passport Builder Score contract PassportBuilderScore public passportBuilderScore; @@ -96,7 +96,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice Create a new Talent Vault contract /// @param _token The token that will be deposited into the contract /// @param _yieldSource The wallet paying for the yield - /// @param _maxYieldAmount The maximum amount of tokens that can be used to calculate interest + /// @param _maxYieldAmount The maximum amount of tokens that can be used to calculate rewards /// @param _passportBuilderScore The Passport Builder Score contract constructor( IERC20 _token, @@ -115,7 +115,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { token = _token; yieldRateBase = 0; yieldSource = _yieldSource; - yieldInterestFlag = true; + yieldRewardsFlag = true; maxYieldAmount = _maxYieldAmount; passportBuilderScore = _passportBuilderScore; lockPeriod = 7 days; @@ -145,12 +145,12 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { removeMaxDepositLimit(receiver); } - /// @notice Calculate any accrued interest for the caller + /// @notice Calculate any accrued rewards for the caller function refresh() external { refreshForAddress(msg.sender); } - /// @notice Withdraws all of the user's balance, including any accrued interest. + /// @notice Withdraws all of the user's balance, including any accrued rewards. function withdrawAll() external nonReentrant { refreshForAddress(msg.sender); redeem(balanceOf(msg.sender), msg.sender, msg.sender); @@ -166,7 +166,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { emit YieldRateUpdated(_yieldRate); } - /// @notice Update the maximum amount of tokens that can be used to calculate interest + /// @notice Update the maximum amount of tokens that can be used to calculate rewards /// @dev Can only be called by the owner /// @param _maxYieldAmount The new maximum yield amount function setMaxYieldAmount(uint256 _maxYieldAmount) external onlyOwner { @@ -175,7 +175,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { emit MaxYieldAmountUpdated(_maxYieldAmount); } - /// @notice Update the time at which the users of the contract will stop accruing interest + /// @notice Update the time at which the users of the contract will stop accruing rewards /// @dev Can only be called by the owner /// @param _yieldAccrualDeadline The new yield accrual deadline function setYieldAccrualDeadline(uint256 _yieldAccrualDeadline) external onlyOwner { @@ -186,16 +186,16 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { emit YieldAccrualDeadlineUpdated(_yieldAccrualDeadline); } - /// @notice Stop the contract from accruing interest + /// @notice Stop the contract from accruing rewards /// @dev Can only be called by the owner - function stopYieldingInterest() external onlyOwner { - yieldInterestFlag = false; + function stopYieldingRewards() external onlyOwner { + yieldRewardsFlag = false; } - /// @notice Start the contract accruing interest + /// @notice Start the contract accruing rewards /// @dev Can only be called by the owner - function startYieldingInterest() external onlyOwner { - yieldInterestFlag = true; + function startYieldingRewards() external onlyOwner { + yieldRewardsFlag = true; } /// @notice Set the yield source for the contract @@ -251,17 +251,17 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { return deposit(shares, receiver); } - /// @notice Calculate any accrued interest for an address and update - /// the deposit meta data including minting any interest + /// @notice Calculate any accrued rewards for an address and update + /// the deposit meta data including minting any rewards /// @param account The address of the user to refresh function refreshForAddress(address account) public { if (balanceOf(account) <= 0) { UserBalanceMeta storage balanceMeta = userBalanceMeta[account]; - balanceMeta.lastInterestCalculation = block.timestamp; + balanceMeta.lastRewardCalculation = block.timestamp; return; } - yieldInterest(account); + yieldRewards(account); } /// @notice Get the yield rate for the contract for a given user @@ -301,12 +301,12 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { revert TalentVaultNonTransferable(); } - /// @notice Calculate the accrued interest for an address - /// @param user The address to calculate the accrued interest for - function calculateInterest(address user) public view returns (uint256) { + /// @notice Calculate the accrued rewards for an address + /// @param user The address to calculate the accrued rewards for + function calculateRewards(address user) public view returns (uint256) { UserBalanceMeta storage balanceMeta = userBalanceMeta[user]; - if (!yieldInterestFlag) { + if (!yieldRewardsFlag) { return 0; } @@ -327,11 +327,11 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { uint256 timeElapsed; if (block.timestamp > endTime) { - timeElapsed = endTime > balanceMeta.lastInterestCalculation - ? endTime - balanceMeta.lastInterestCalculation + timeElapsed = endTime > balanceMeta.lastRewardCalculation + ? endTime - balanceMeta.lastRewardCalculation : 0; } else { - timeElapsed = block.timestamp - balanceMeta.lastInterestCalculation; + timeElapsed = block.timestamp - balanceMeta.lastRewardCalculation; } uint256 yieldRate = getYieldRateForScore(user); @@ -358,14 +358,14 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { delete maxDepositLimitFlags[receiver]; } - /// @notice Calculate the accrued interest for an address and mint any interest - /// @param user The address to calculate the accrued interest for - function yieldInterest(address user) internal { + /// @notice Calculate the accrued rewards for an address and mint any rewards + /// @param user The address to calculate the accrued rewards for + function yieldRewards(address user) internal { UserBalanceMeta storage balanceMeta = userBalanceMeta[user]; - uint256 interest = calculateInterest(user); - balanceMeta.lastInterestCalculation = block.timestamp; + uint256 rewards = calculateRewards(user); + balanceMeta.lastRewardCalculation = block.timestamp; - _deposit(yieldSource, user, interest, interest); + _deposit(yieldSource, user, rewards, rewards); } /// @notice Withdraws tokens from the contract diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 5dc9757e..611b57d5 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -101,7 +101,7 @@ describe("TalentVault", () => { expect(await talentVault.passportBuilderScore()).not.to.equal(ethers.constants.AddressZero); expect(await talentVault.passportBuilderScore()).to.equal(passportBuilderScore.address); - expect(await talentVault.yieldInterestFlag()).to.equal(true); + expect(await talentVault.yieldRewardsFlag()).to.equal(true); expect(await talentVault.lockPeriod()).to.equal(7 * 24 * 60 * 60); }); @@ -622,33 +622,32 @@ describe("TalentVault", () => { describe("#refreshForAddress", async () => { context("when address does not have a deposit", async () => { - it("just updates the last interest calculation", async () => { - const lastInterestCalculationBefore = (await talentVault.userBalanceMeta(user3.address)) - .lastInterestCalculation; + it("just updates the last reward calculation", async () => { + const lastRewardCalculationBefore = (await talentVault.userBalanceMeta(user3.address)).lastRewardCalculation; - expect(lastInterestCalculationBefore).to.equal(0); + expect(lastRewardCalculationBefore).to.equal(0); // fire await talentVault.refreshForAddress(user3.address); - const lastInterestCalculation = (await talentVault.userBalanceMeta(user3.address)).lastInterestCalculation; + const lastRewardCalculation = (await talentVault.userBalanceMeta(user3.address)).lastRewardCalculation; - expect(lastInterestCalculation).not.to.equal(0); + expect(lastRewardCalculation).not.to.equal(0); }); }); - // Make sure user balance is updated according to yielded interest. This is done in the - // tests below, in the Interest Calculation tests, where we call #refresh + // Make sure user balance is updated according to yielded rewards. This is done in the + // tests below, in the Rewards Calculation tests, where we call #refresh }); // withdrawAll // // $TALENT for user is increased by their $TALENTVAULT balance - // which is updated with the yield interest. + // which is updated with the yield rewards. // // TalentVault $TALENT balance is reduced by the originally deposited amount // - // yieldSource $TALENT balance is reduced by the yieldInterest + // yieldSource $TALENT balance is reduced by the yieldRewards // // user $TALENTVAULT balance goes to 0. @@ -674,10 +673,10 @@ describe("TalentVault", () => { // Simulate time passing ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - const yieldedInterest = depositAmount.mul(0).div(100); // 0% interest + const yieldedRewards = depositAmount.mul(0).div(100); // 0% rewards // this is manually calculated, but it is necessary for this test. - const expectedUser1TalentVaultBalanceAfter1Year = ethers.utils.parseEther("1000"); // there is no interest + const expectedUser1TalentVaultBalanceAfter1Year = ethers.utils.parseEther("1000"); // there are no rewards // fire await talentVault.connect(user1).withdrawAll(); @@ -698,9 +697,9 @@ describe("TalentVault", () => { const user1TalentVaultBalanceAfter = await talentVault.balanceOf(user1.address); expect(user1TalentVaultBalanceAfter).to.equal(0); - // yieldSource $TALENT balance is decreased by the yieldInterest + // yieldSource $TALENT balance is decreased by the yieldRewards const yieldSourceTalentBalanceAfter = await talentToken.balanceOf(yieldSource.address); - const expectedYieldSourceTalentBalanceAfter = yieldSourceTalentBalanceBefore.sub(yieldedInterest); + const expectedYieldSourceTalentBalanceAfter = yieldSourceTalentBalanceBefore.sub(yieldedRewards); expect(yieldSourceTalentBalanceAfter).to.be.closeTo( expectedYieldSourceTalentBalanceAfter, ethers.utils.parseEther("0.001") @@ -708,8 +707,8 @@ describe("TalentVault", () => { }); }); - describe("Interest Calculation", async () => { - it("Should calculate interest correctly", async () => { + describe("Rewards Calculation", async () => { + it("Should calculate rewards correctly", async () => { const depositAmount = ethers.utils.parseEther("1000"); await talentToken.transfer(user1.address, depositAmount); await talentToken.connect(user1).approve(talentVault.address, depositAmount); @@ -718,29 +717,29 @@ describe("TalentVault", () => { // Simulate time passing ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - const expectedInterest = depositAmount.mul(0).div(100); // 0% interest + const expectedRewards = depositAmount.mul(0).div(100); // 0% rewards // fire await talentVault.connect(user1).refresh(); const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.001")); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedRewards), ethers.utils.parseEther("0.001")); - const userLastInterestCalculation = (await talentVault.userBalanceMeta(user1.address)).lastInterestCalculation; + const userLastRewardCalculation = (await talentVault.userBalanceMeta(user1.address)).lastRewardCalculation; const oneYearAfterEpochSeconds = currentDateEpochSeconds + 31536000; - expect(userLastInterestCalculation.toNumber()).to.equal(oneYearAfterEpochSeconds); + expect(userLastRewardCalculation.toNumber()).to.equal(oneYearAfterEpochSeconds); }); - context("when yielding interest is stopped", async () => { - it("does not yield any interest but it updates the lastInterestCalculation", async () => { + context("when yielding rewards is stopped", async () => { + it("does not yield any rewards but it updates the lastRewardCalculation", async () => { const depositAmount = ethers.utils.parseEther("1000"); await talentToken.transfer(user1.address, depositAmount); const user1BalanceBefore = await talentToken.balanceOf(user1.address); await talentToken.connect(user1).approve(talentVault.address, depositAmount); await talentVault.connect(user1).deposit(depositAmount, user1.address); - await talentVault.stopYieldingInterest(); + await talentVault.stopYieldingRewards(); // Simulate time passing @@ -752,15 +751,15 @@ describe("TalentVault", () => { const user1BalanceAfter = await talentVault.balanceOf(user1.address); expect(user1BalanceAfter).to.equal(user1BalanceBefore); - const userLastInterestCalculation = (await talentVault.userBalanceMeta(user1.address)).lastInterestCalculation; + const userLastRewardCalculation = (await talentVault.userBalanceMeta(user1.address)).lastRewardCalculation; const oneYearAfterEpochSeconds = currentDateEpochSeconds + 31536000; - expect(userLastInterestCalculation.toNumber()).to.equal(oneYearAfterEpochSeconds); + expect(userLastRewardCalculation.toNumber()).to.equal(oneYearAfterEpochSeconds); }); }); // 10000 - it("Should calculate interest even if amount is above the max yield amount correctly", async () => { + it("Should calculate rewards even if amount is above the max yield amount correctly", async () => { const depositAmount = ethers.utils.parseEther("15000"); const maxAmount = ethers.utils.parseEther("10000"); await talentToken.transfer(user1.address, depositAmount); @@ -770,16 +769,16 @@ describe("TalentVault", () => { // Simulate time passing ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - const expectedInterest = maxAmount.mul(0).div(100); // 0% interest + const expectedRewards = maxAmount.mul(0).div(100); // 0% rewards // fire await talentVault.connect(user1).refresh(); const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.001")); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedRewards), ethers.utils.parseEther("0.001")); }); - it("Should calculate interest correctly for builders with scores above 50 but below 75", async () => { + it("Should calculate rewards correctly for builders with scores above 50 but below 75", async () => { await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode await passportRegistry.connect(user1).create("source1"); @@ -796,12 +795,12 @@ describe("TalentVault", () => { // fire await talentVault.connect(user1).refresh(); - const expectedInterest = depositAmount.mul(5).div(100); // 5% interest + const expectedRewards = depositAmount.mul(5).div(100); // 5% rewards const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedRewards), ethers.utils.parseEther("0.1")); }); - it("Should calculate interest correctly for builders with scores above 75 but below 100", async () => { + it("Should calculate rewards correctly for builders with scores above 75 but below 100", async () => { await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode await passportRegistry.connect(user1).create("source1"); @@ -818,12 +817,12 @@ describe("TalentVault", () => { // fire await talentVault.connect(user1).refresh(); - const expectedInterest = depositAmount.mul(10).div(100); // 10% interest + const expectedRewards = depositAmount.mul(10).div(100); // 10% rewards const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedRewards), ethers.utils.parseEther("0.1")); }); - it("Should calculate interest correctly for builders with scores above 100", async () => { + it("Should calculate rewards correctly for builders with scores above 100", async () => { await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode await passportRegistry.connect(user1).create("source1"); @@ -840,9 +839,9 @@ describe("TalentVault", () => { // fire await talentVault.connect(user1).refresh(); - const expectedInterest = depositAmount.mul(15).div(100); // 15% interest + const expectedRewards = depositAmount.mul(15).div(100); // 15% rewards const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.be.closeTo(depositAmount.add(expectedInterest), ethers.utils.parseEther("0.1")); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedRewards), ethers.utils.parseEther("0.1")); }); }); @@ -861,38 +860,38 @@ describe("TalentVault", () => { }); }); - describe("#stopYieldingInterest", async () => { + describe("#stopYieldingRewards", async () => { context("when called by an non-owner account", async () => { it("reverts", async () => { - await expect(talentVault.connect(user1).stopYieldingInterest()).to.be.revertedWith( + await expect(talentVault.connect(user1).stopYieldingRewards()).to.be.revertedWith( `OwnableUnauthorizedAccount("${user1.address}")` ); }); }); context("when called by the owner account", async () => { - it("stops yielding interest", async () => { - await talentVault.stopYieldingInterest(); + it("stops yielding rewards", async () => { + await talentVault.stopYieldingRewards(); - expect(await talentVault.yieldInterestFlag()).to.equal(false); + expect(await talentVault.yieldRewardsFlag()).to.equal(false); }); }); }); - describe("#startYieldingInterest", async () => { + describe("#startYieldingRewards", async () => { context("when called by an non-owner account", async () => { it("reverts", async () => { - await expect(talentVault.connect(user1).startYieldingInterest()).to.be.revertedWith( + await expect(talentVault.connect(user1).startYieldingRewards()).to.be.revertedWith( `OwnableUnauthorizedAccount("${user1.address}")` ); }); }); context("when called by the owner account", async () => { - it("starts yielding interest", async () => { - await talentVault.startYieldingInterest(); + it("starts yielding rewards", async () => { + await talentVault.startYieldingRewards(); - expect(await talentVault.yieldInterestFlag()).to.equal(true); + expect(await talentVault.yieldRewardsFlag()).to.equal(true); }); }); }); From 21b8c797ca1ca347eb42af3825cbd56912efcc11 Mon Sep 17 00:00:00 2001 From: Ruben Dinis Date: Wed, 4 Dec 2024 16:46:58 +0000 Subject: [PATCH 67/74] Remove function not needed in TalentTGEUnlock --- contracts/talent/TalentTGEUnlock.sol | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/contracts/talent/TalentTGEUnlock.sol b/contracts/talent/TalentTGEUnlock.sol index bbcc63bf..179bb789 100644 --- a/contracts/talent/TalentTGEUnlock.sol +++ b/contracts/talent/TalentTGEUnlock.sol @@ -61,12 +61,11 @@ contract TalentTGEUnlock is Ownable { verifyAmount(merkleProofClaim, amountAllocated); address beneficiary = msg.sender; - uint256 amountToClaim = calculate(beneficiary, amountAllocated); - claimed[beneficiary] += amountToClaim; - IERC20(token).safeTransfer(beneficiary, amountToClaim); + claimed[beneficiary] += amountAllocated; + IERC20(token).safeTransfer(beneficiary, amountAllocated); - emit Claimed(beneficiary, amountToClaim, 0); + emit Claimed(beneficiary, amountAllocated, 0); } function verifyAmount( @@ -84,15 +83,6 @@ contract TalentTGEUnlock is Ownable { ); } - function calculate( - address beneficiary, - uint256 amountAllocated - ) internal view returns (uint256 amountToClaim) { - uint256 amountClaimed = claimed[beneficiary]; - assert(amountClaimed <= amountAllocated); - amountToClaim = amountAllocated - amountClaimed; - } - function setMerkleRoot(bytes32 nextMerkleRoot) external onlyOwner { merkleRoot = nextMerkleRoot; } From a39b0c1fcd3366c8a9deee6171f512520e68d9c9 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Wed, 15 Jan 2025 17:32:34 +0000 Subject: [PATCH 68/74] Add simple ERC4626 contract to stake and receive sTALENT --- contracts/talent/TalentVaultV2.sol | 34 ++++++++ test/contracts/talent/TalentVaultV2.ts | 103 +++++++++++++++++++++++++ test/shared/artifacts.ts | 2 + 3 files changed, 139 insertions(+) create mode 100644 contracts/talent/TalentVaultV2.sol create mode 100644 test/contracts/talent/TalentVaultV2.ts diff --git a/contracts/talent/TalentVaultV2.sol b/contracts/talent/TalentVaultV2.sol new file mode 100644 index 00000000..7ea9fbf5 --- /dev/null +++ b/contracts/talent/TalentVaultV2.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; + +contract TalentVaultV2 is ERC4626 { + using SafeERC20 for IERC20; + IERC20 public immutable token; + + constructor(ERC20 _asset) + ERC4626(_asset) + ERC20("Staked TALENT", "sTALENT") + { + token = _asset; + } + + // Given the 1:1 ratio, we can simply map assets to shares directly. + function convertToShares(uint256 assets) public view virtual override returns (uint256) { + return assets; + } + + // Likewise, shares map back to the same number of assets. + function convertToAssets(uint256 shares) public view virtual override returns (uint256) { + return shares; + } + + // Total assets is simply the balance of the token in this contract. + function totalAssets() public view virtual override returns (uint256) { + return token.balanceOf(address(this)); + } +} \ No newline at end of file diff --git a/test/contracts/talent/TalentVaultV2.ts b/test/contracts/talent/TalentVaultV2.ts new file mode 100644 index 00000000..61990a6f --- /dev/null +++ b/test/contracts/talent/TalentVaultV2.ts @@ -0,0 +1,103 @@ +import chai from "chai"; +import { ethers, waffle } from "hardhat"; +import { solidity } from "ethereum-waffle"; + +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { TalentProtocolToken, TalentVaultV2, PassportRegistry, PassportBuilderScore } from "../../../typechain-types"; +import { Artifacts } from "../../shared"; +import { ensureTimestamp } from "../../shared/utils"; + +chai.use(solidity); + +const { expect } = chai; +const { deployContract } = waffle; + +async function ensureTimeIsAfterLockPeriod() { + const lockPeriod = 8; + const oneDayAfterLockPeriod = Math.floor(Date.now() / 1000) + lockPeriod * 24 * 60 * 60; + await ensureTimestamp(oneDayAfterLockPeriod); +} + +describe("TalentVaultV2", () => { + let admin: SignerWithAddress; + let yieldSource: SignerWithAddress; + let user1: SignerWithAddress; + let user2: SignerWithAddress; + let user3: SignerWithAddress; + + let talentToken: TalentProtocolToken; + let talentVault: TalentVaultV2; + + beforeEach(async () => { + await ethers.provider.send("hardhat_reset", []); + + [admin, yieldSource, user1, user2, user3] = await ethers.getSigners(); + + talentToken = (await deployContract(admin, Artifacts.TalentProtocolToken, [admin.address])) as TalentProtocolToken; + + talentVault = (await deployContract(admin, Artifacts.TalentVaultV2, [talentToken.address])) as TalentVaultV2; + + console.log("------------------------------------"); + console.log("Addresses:"); + console.log(`admin = ${admin.address}`); + console.log(`user1 = ${user1.address}`); + console.log(`user2 = ${user2.address}`); + console.log(`user3 = ${user3.address}`); + console.log(`talentToken = ${talentToken.address}`); + console.log(`talentVault = ${talentVault.address}`); + console.log("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); + + await talentToken.unpause(); + }); + + it("should deposit TALENT and mint sTALENT at 1:1 ratio", async () => { + // transfer TALENT to user + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.connect(admin).transfer(user1.address, depositAmount); + + // Approve the vault to spend user's TALENT + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + + // Check initial balances + const userInitialTalent = await talentToken.balanceOf(user1.address); + const userInitialShares = await talentVault.balanceOf(user1.address); + + // User deposits 100 TALENT + const depositedAmount = ethers.utils.parseEther("100"); + await talentVault.connect(user1).deposit(depositedAmount, user1.address); + + // Check the user's final balance of TALENT + const userFinalTalent = await talentToken.balanceOf(user1.address); + const userFinalShares = await talentVault.balanceOf(user1.address); + + expect(userInitialTalent.sub(depositedAmount)).to.eq(userFinalTalent); + expect(userFinalShares.sub(userInitialShares)).to.eq(depositedAmount); + }); + + it("should redeem sTALENT back to TALENT at 1:1 ratio", async () => { + // User redeems 50 sTALENT + const redeemAmount = ethers.utils.parseEther("50"); + + // transfer TALENT to user + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.connect(admin).transfer(user1.address, depositAmount); + + // Approve the vault to spend user's TALENT + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + + // deposit TALENT + await talentVault.connect(user1).deposit(depositAmount, user1.address); + + const userInitialTalent = await talentToken.balanceOf(user1.address); + const userInitialShares = await talentVault.balanceOf(user1.address); + + // redeem sTALENT + await talentVault.connect(user1).redeem(redeemAmount, user1.address, user1.address); + + const userFinalTalent = await talentToken.balanceOf(user1.address); + const userFinalShares = await talentVault.balanceOf(user1.address); + + expect(userFinalTalent.sub(userInitialTalent)).to.eq(redeemAmount); + expect(userInitialShares.sub(userFinalShares)).to.eq(redeemAmount); + }); +}); diff --git a/test/shared/artifacts.ts b/test/shared/artifacts.ts index 01fd525b..86782114 100644 --- a/test/shared/artifacts.ts +++ b/test/shared/artifacts.ts @@ -11,6 +11,7 @@ import TalentTGEUnlock from "../../artifacts/contracts/talent/TalentTGEUnlock.so import PassportWalletRegistry from "../../artifacts/contracts/passport/PassportWalletRegistry.sol/PassportWalletRegistry.json"; import TalentTGEUnlockTimestamp from "../../artifacts/contracts/talent/TalentTGEUnlockTimestamp.sol/TalentTGEUnlockTimestamp.json"; import TalentVault from "../../artifacts/contracts/talent/TalentVault.sol/TalentVault.json"; +import TalentVaultV2 from "../../artifacts/contracts/talent/TalentVaultV2.sol/TalentVaultV2.json"; export { PassportRegistry, @@ -26,4 +27,5 @@ export { PassportWalletRegistry, TalentTGEUnlockTimestamp, TalentVault, + TalentVaultV2, }; From 5cc1c23523efd5e78f0034837c589fcfaa12799b Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Wed, 15 Jan 2025 17:52:23 +0000 Subject: [PATCH 69/74] Add whitelisting of vault strategies/options --- contracts/talent/TalentVaultV2.sol | 44 ++++++++++++++++++++++++-- test/contracts/talent/TalentVaultV2.ts | 24 ++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/contracts/talent/TalentVaultV2.sol b/contracts/talent/TalentVaultV2.sol index 7ea9fbf5..851cd722 100644 --- a/contracts/talent/TalentVaultV2.sol +++ b/contracts/talent/TalentVaultV2.sol @@ -1,22 +1,62 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; +import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; -contract TalentVaultV2 is ERC4626 { +contract TalentVaultV2 is ERC4626, Ownable { using SafeERC20 for IERC20; IERC20 public immutable token; + // Mapping of strategy address => boolean indicating whitelist status + // This is used to prevent users from depositing tokens from untrusted strategies + mapping(address => bool) private _whitelistedStrategies; + + event VaultOptionAdded(address indexed vaultOption); + event VaultOptionRemoved(address indexed vaultOption); + constructor(ERC20 _asset) ERC4626(_asset) ERC20("Staked TALENT", "sTALENT") + Ownable(msg.sender) { token = _asset; } + /** + * @dev Adds a new yield strategy to the whitelist. + * Can only be called by the contract owner (admin). + */ + function addVaultOption(address vaultOption) external onlyOwner { + require(vaultOption != address(0), "Invalid address"); + require(!_whitelistedStrategies[vaultOption], "Already whitelisted"); + + _whitelistedStrategies[vaultOption] = true; + emit VaultOptionAdded(vaultOption); + } + + /** + * @dev Removes a yield strategy from the whitelist. + * Can only be called by the contract owner (admin). + */ + function removeVaultOption(address vaultOption) external onlyOwner { + require(_whitelistedStrategies[vaultOption], "Not whitelisted"); + + _whitelistedStrategies[vaultOption] = false; + + emit VaultOptionRemoved(vaultOption); + } + + /** + * @dev Returns true if `vaultOption` is whitelisted. + */ + function isWhitelisted(address vaultOption) public view returns (bool) { + return _whitelistedStrategies[vaultOption]; + } + // Given the 1:1 ratio, we can simply map assets to shares directly. function convertToShares(uint256 assets) public view virtual override returns (uint256) { return assets; @@ -31,4 +71,4 @@ contract TalentVaultV2 is ERC4626 { function totalAssets() public view virtual override returns (uint256) { return token.balanceOf(address(this)); } -} \ No newline at end of file +} diff --git a/test/contracts/talent/TalentVaultV2.ts b/test/contracts/talent/TalentVaultV2.ts index 61990a6f..8a04391e 100644 --- a/test/contracts/talent/TalentVaultV2.ts +++ b/test/contracts/talent/TalentVaultV2.ts @@ -100,4 +100,28 @@ describe("TalentVaultV2", () => { expect(userFinalTalent.sub(userInitialTalent)).to.eq(redeemAmount); expect(userInitialShares.sub(userFinalShares)).to.eq(redeemAmount); }); + + it("should set the vault option as true when a vault option is added", async () => { + await talentVault.connect(admin).addVaultOption(user2.address); + expect(await talentVault.isWhitelisted(user2.address)).to.be.true; + }); + + it("should set the vault option as false when a vault option is removed", async () => { + await talentVault.connect(admin).addVaultOption(user2.address); + expect(await talentVault.isWhitelisted(user2.address)).to.be.true; + + await talentVault.connect(admin).removeVaultOption(user2.address); + expect(await talentVault.isWhitelisted(user2.address)).to.be.false; + }); + + it("should emit VaultOptionAdded event when a vault option is added", async () => { + const addVaultOptionTx = talentVault.connect(admin).addVaultOption(user2.address); + await expect(addVaultOptionTx).to.emit(talentVault, "VaultOptionAdded").withArgs(user2.address); + }); + + it("should emit VaultOptionRemoved event when a vault option is removed", async () => { + await talentVault.connect(admin).addVaultOption(user2.address); + const removeVaultOptionTx = talentVault.connect(admin).removeVaultOption(user2.address); + await expect(removeVaultOptionTx).to.emit(talentVault, "VaultOptionRemoved").withArgs(user2.address); + }); }); From c014f02677794e2e8deeaf8120322841e3e05e0f Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Thu, 16 Jan 2025 15:56:56 +0000 Subject: [PATCH 70/74] Add VaultOption with integration test --- contracts/talent/TalentVaultV2.sol | 42 +++++++ contracts/talent/vault-options/BaseAPY.sol | 112 ++++++++++++++++++ .../talent/vault-options/IVaultOption.sol | 25 ++++ .../talent/vault-integration/BaseAPY.ts | 97 +++++++++++++++ test/shared/artifacts.ts | 2 + 5 files changed, 278 insertions(+) create mode 100644 contracts/talent/vault-options/BaseAPY.sol create mode 100644 contracts/talent/vault-options/IVaultOption.sol create mode 100644 test/contracts/talent/vault-integration/BaseAPY.ts diff --git a/contracts/talent/TalentVaultV2.sol b/contracts/talent/TalentVaultV2.sol index 851cd722..231e7d98 100644 --- a/contracts/talent/TalentVaultV2.sol +++ b/contracts/talent/TalentVaultV2.sol @@ -6,6 +6,7 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; +import "./vault-options/IVaultOption.sol"; contract TalentVaultV2 is ERC4626, Ownable { using SafeERC20 for IERC20; @@ -34,6 +35,8 @@ contract TalentVaultV2 is ERC4626, Ownable { require(vaultOption != address(0), "Invalid address"); require(!_whitelistedStrategies[vaultOption], "Already whitelisted"); + token.approve(vaultOption, type(uint256).max); + _whitelistedStrategies[vaultOption] = true; emit VaultOptionAdded(vaultOption); } @@ -71,4 +74,43 @@ contract TalentVaultV2 is ERC4626, Ownable { function totalAssets() public view virtual override returns (uint256) { return token.balanceOf(address(this)); } + + function depositToOption( + uint256 assets, + address receiver, + address option + ) external returns (uint256 shares) { + require(_whitelistedStrategies[option], "Option not whitelisted"); + + // Deposit into the Vault which + // 1. Transfer TALENT from user -> vault + // 2. The vault mints sTALENT for the user, 1:1 + shares = deposit(assets, receiver); + // 3. Vault calls the option’s deposit function which transfers the TALENT to the option + IVaultOption(option).depositIntoVaultOption(receiver, assets); + return shares; + } + + function withdrawFromOption( + uint256 shares, + address receiver, + address owner, + address option + ) public returns (uint256 assets) { + // Check that option is whitelisted + require(_whitelistedStrategies[option], "Option not whitelisted"); + + // Option calculates principal + yield and sends it to the vault + uint256 rewardsOwed = IVaultOption(option).withdrawFromVaultOption(owner); + // Withdraw from the Vault which + // 1. Burns the user's shares + // 2. Transfers TALENT from the vault -> user + assets = withdraw(shares, receiver, owner); + + // The vault then sends rewarded TALENT to the user + bool success = token.transfer(receiver, rewardsOwed); + require(success, "reward transfer failed"); + + return assets + rewardsOwed; + } } diff --git a/contracts/talent/vault-options/BaseAPY.sol b/contracts/talent/vault-options/BaseAPY.sol new file mode 100644 index 00000000..e64917cf --- /dev/null +++ b/contracts/talent/vault-options/BaseAPY.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./IVaultOption.sol"; + +/** + * @dev A super-simplistic “10% APY” strategy. + * Assumes no compounding. Tracks each user’s deposit time & amount. + * On withdrawal, calculates pro-rata interest and sends to the vault. + */ +contract BaseAPY is IVaultOption { + IERC20 public immutable talentToken; + address public immutable vault; // The vault that interacts with this strategy + + uint256 private constant YEAR_IN_SECONDS = 365 days; + uint256 private constant APY = 10_00; // 10% + uint256 private constant APY_PRECISION = 100_00; // 100% + + // Track deposit details for each user + struct DepositInfo { + uint256 amount; // How many TALENT deposited + uint256 depositTime; // When it was deposited + } + mapping(address => DepositInfo) public deposits; + + event DepositOption(address indexed user, uint256 amount); + event WithdrawOption(address indexed user, uint256 amount); + + modifier onlyVault() { + require(msg.sender == vault, "Not authorized"); + _; + } + + // TODO: + // 1. add min/max deposit amounts + // 2. add lock period + // 3. add max number of deposits + constructor(address _talentToken, address _vault) { + require(_talentToken != address(0), "Invalid token"); + require(_vault != address(0), "Invalid vault"); + + talentToken = IERC20(_talentToken); + vault = _vault; + } + + /** + * @notice Vault calls this when user deposits TALENT into the strategy. + */ + function depositIntoVaultOption(address user, uint256 amount) external override onlyVault { + require(amount > 0, "Cannot deposit zero"); + + // Transfer TALENT from vault to this strategy + bool success = talentToken.transferFrom(vault, address(this), amount); + require(success, "Transfer failed"); + + // If user already had some deposit, we add to it and recalc partial interest. + if (deposits[user].amount > 0) { + uint256 timeElapsed = block.timestamp - deposits[user].depositTime; + uint256 interest = (deposits[user].amount * APY * timeElapsed) / (APY_PRECISION * YEAR_IN_SECONDS); + deposits[user].amount += interest; + } + deposits[user].amount += amount; + deposits[user].depositTime = block.timestamp; + + emit DepositOption(user, amount); + } + + /** + * @notice Vault calls this when user withdraws from the strategy. + * @return interest The total TALENT owed to the user (principal + yield). + */ + function withdrawFromVaultOption(address user) external override onlyVault returns (uint256 interest) { + DepositInfo memory info = deposits[user]; + require(info.amount > 0, "No deposit for user"); + + // Calculate how long the user has been in the strategy + uint256 timeElapsed = block.timestamp - info.depositTime; + + // Simple interest = principal * APY * (timeElapsed / YEAR_IN_SECONDS) + // APY = 10% => factor = 0.10 + // but we keep math in integer form: interest = (amount * 10 * timeElapsed) / (100 * YEAR_IN_SECONDS) + interest = (info.amount * APY * timeElapsed) / (APY_PRECISION * YEAR_IN_SECONDS); + + uint256 totalOwed = info.amount + interest; + + // Reset the user deposit + deposits[user].amount = 0; + deposits[user].depositTime = 0; + + // Transfer TALENT to the vault (the vault will handle final distribution to user) + bool success = talentToken.transfer(vault, totalOwed); + require(success, "Transfer failed"); + + emit WithdrawOption(user, interest); + + return interest; + } + + /** + * @notice Preview how much TALENT (principal + yield) user would get if they withdraw now. + */ + function previewRewards(address user) external view override returns (uint256) { + DepositInfo memory info = deposits[user]; + if (info.amount == 0) { + return 0; + } + uint256 timeElapsed = block.timestamp - info.depositTime; + uint256 interest = (info.amount * APY * timeElapsed) / (APY_PRECISION * YEAR_IN_SECONDS); + return info.amount + interest; + } +} \ No newline at end of file diff --git a/contracts/talent/vault-options/IVaultOption.sol b/contracts/talent/vault-options/IVaultOption.sol new file mode 100644 index 00000000..f3e58c1b --- /dev/null +++ b/contracts/talent/vault-options/IVaultOption.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IVaultOption { + /** + * @notice Called by the vault when user deposits TALENT to this strategy. + * @param user The user who is depositing. + * @param amount The amount of TALENT (in wei) being deposited. + */ + function depositIntoVaultOption(address user, uint256 amount) external; + + /** + * @notice Called by the vault when user withdraws from this strategy. + * @dev Vault option should return the total principal + yield to the vault. + * @param user The user who is withdrawing. + * @return totalOwed The total TALENT owed to the user (principal + yield). + */ + function withdrawFromVaultOption(address user) external returns (uint256 totalOwed); + + /** + * @notice Preview how much TALENT (principal + yield) `user` has if they were to withdraw now. + * @param user The user to check. + */ + function previewRewards(address user) external view returns (uint256); +} diff --git a/test/contracts/talent/vault-integration/BaseAPY.ts b/test/contracts/talent/vault-integration/BaseAPY.ts new file mode 100644 index 00000000..2cfe1dc7 --- /dev/null +++ b/test/contracts/talent/vault-integration/BaseAPY.ts @@ -0,0 +1,97 @@ +import chai from "chai"; +import { ethers, waffle } from "hardhat"; +import { solidity } from "ethereum-waffle"; + +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { TalentProtocolToken, TalentVaultV2, BaseAPY } from "../../../../typechain-types"; +import { Artifacts } from "../../../shared"; +import { ensureTimestamp } from "../../../shared/utils"; + +chai.use(solidity); + +const { expect } = chai; +const { deployContract } = waffle; + +async function ensureTimeIsAfterLockPeriod() { + const lockPeriod = 8; + const oneDayAfterLockPeriod = Math.floor(Date.now() / 1000) + lockPeriod * 24 * 60 * 60; + await ensureTimestamp(oneDayAfterLockPeriod); +} + +describe("TalentVaultV2", () => { + let admin: SignerWithAddress; + let yieldSource: SignerWithAddress; + let user1: SignerWithAddress; + let user2: SignerWithAddress; + let user3: SignerWithAddress; + + let talentToken: TalentProtocolToken; + let talentVault: TalentVaultV2; + let baseAPY: BaseAPY; + + beforeEach(async () => { + await ethers.provider.send("hardhat_reset", []); + + [admin, yieldSource, user1, user2, user3] = await ethers.getSigners(); + + talentToken = (await deployContract(admin, Artifacts.TalentProtocolToken, [admin.address])) as TalentProtocolToken; + + talentVault = (await deployContract(admin, Artifacts.TalentVaultV2, [talentToken.address])) as TalentVaultV2; + + baseAPY = (await deployContract(admin, Artifacts.BaseAPY, [talentToken.address, talentVault.address])) as BaseAPY; + + console.log("------------------------------------"); + console.log("Addresses:"); + console.log(`admin = ${admin.address}`); + console.log(`user1 = ${user1.address}`); + console.log(`user2 = ${user2.address}`); + console.log(`user3 = ${user3.address}`); + console.log(`talentToken = ${talentToken.address}`); + console.log(`talentVault = ${talentVault.address}`); + console.log(`baseAPY = ${baseAPY.address}`); + console.log("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); + + await talentToken.unpause(); + }); + + it("should deposit TALENT and mint sTALENT at 1:1 ratio", async () => { + // whitelist baseAPY + await talentVault.connect(admin).addVaultOption(baseAPY.address); + + // transfer TALENT to user + const depositAmount = ethers.utils.parseEther("1000"); + await talentToken.connect(admin).transfer(user1.address, depositAmount); + + // Approve the vault to spend user's TALENT + await talentToken.connect(user1).approve(talentVault.address, depositAmount); + + // transfer TALENT to baseAPY + const rewardAmount = ethers.utils.parseEther("100"); + await talentToken.connect(admin).transfer(baseAPY.address, rewardAmount); + + // Check initial balances + const userInitialTalent = await talentToken.balanceOf(user1.address); + const userInitialShares = await talentVault.balanceOf(user1.address); + const baseAPYInitialTalent = await talentToken.balanceOf(baseAPY.address); + + // User deposits 100 TALENT to the baseAPY position + const depositedAmount = ethers.utils.parseEther("100"); + await talentVault.connect(user1).depositToOption(depositedAmount, user1.address, baseAPY.address); + + // Check the user's final balance of TALENT + const userFinalTalent = await talentToken.balanceOf(user1.address); + const userFinalShares = await talentVault.balanceOf(user1.address); + const baseAPYFinalTalent = await talentToken.balanceOf(baseAPY.address); + const vaultFinalTalent = await talentToken.balanceOf(talentVault.address); + + // user spent 100 TALENT, and received 100 sTALENT + expect(userInitialTalent.sub(depositedAmount)).to.eq(userFinalTalent); + expect(userFinalShares.sub(userInitialShares)).to.eq(depositedAmount); + + // baseAPY holds the Talent transferred from user + expect(baseAPYFinalTalent.sub(baseAPYInitialTalent)).to.eq(depositedAmount); + + // vault holds 0 TALENT because it was transferred to baseAPY + expect(vaultFinalTalent).to.eq(0); + }); +}); diff --git a/test/shared/artifacts.ts b/test/shared/artifacts.ts index 86782114..308172cf 100644 --- a/test/shared/artifacts.ts +++ b/test/shared/artifacts.ts @@ -12,6 +12,7 @@ import PassportWalletRegistry from "../../artifacts/contracts/passport/PassportW import TalentTGEUnlockTimestamp from "../../artifacts/contracts/talent/TalentTGEUnlockTimestamp.sol/TalentTGEUnlockTimestamp.json"; import TalentVault from "../../artifacts/contracts/talent/TalentVault.sol/TalentVault.json"; import TalentVaultV2 from "../../artifacts/contracts/talent/TalentVaultV2.sol/TalentVaultV2.json"; +import BaseAPY from "../../artifacts/contracts/talent/vault-options/BaseAPY.sol/BaseAPY.json"; export { PassportRegistry, @@ -28,4 +29,5 @@ export { TalentTGEUnlockTimestamp, TalentVault, TalentVaultV2, + BaseAPY, }; From 87f742fddf445428c027f4e3e47e4ad577995ad5 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Fri, 17 Jan 2025 10:08:35 +0000 Subject: [PATCH 71/74] Update TalentVault for new requirements --- contracts/talent/TalentVault.sol | 51 ++++++++----------- test/contracts/talent/TalentVault.ts | 75 ++++++---------------------- 2 files changed, 37 insertions(+), 89 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 82229999..b3c15a71 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -19,10 +19,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @param yieldRate The new yield rate event YieldRateUpdated(uint256 yieldRate); - /// @notice Emitted when the maximum yield amount is updated - /// @param maxYieldAmount The new maximum yield amount - event MaxYieldAmountUpdated(uint256 maxYieldAmount); - /// @notice Emitted when the yield accrual deadline is updated /// @param yieldAccrualDeadline The new yield accrual deadline event YieldAccrualDeadlineUpdated(uint256 yieldAccrualDeadline); @@ -36,7 +32,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { error NoDepositFound(); error TalentVaultNonTransferable(); error TransferFailed(); - + error MaxOverallDepositReached(); /// @notice Represents user's balance meta data /// @param depositedAmount The amount of tokens that were deposited, excluding rewards /// @param lastRewardCalculation The timestamp (seconds since Epoch) of the last rewards calculation @@ -50,6 +46,9 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// Lock period end-day is calculated base on the last datetime user did a deposit. uint256 public lockPeriod; + /// @notice The maximum amount of tokens that can be deposited into the vault + uint256 public maxOverallDeposit; + /// @notice The number of seconds in a day uint256 internal constant SECONDS_WITHIN_DAY = 86400; @@ -72,9 +71,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @dev Represented with 2 decimal places, e.g. 10_00 for 10% uint256 public yieldRateBase; - /// @notice The maximum amount of tokens that can be used to calculate reward. - uint256 public maxYieldAmount; - /// @notice The time at which the users of the contract will stop accruing rewards uint256 public yieldAccrualDeadline; @@ -96,12 +92,10 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice Create a new Talent Vault contract /// @param _token The token that will be deposited into the contract /// @param _yieldSource The wallet paying for the yield - /// @param _maxYieldAmount The maximum amount of tokens that can be used to calculate rewards /// @param _passportBuilderScore The Passport Builder Score contract constructor( IERC20 _token, address _yieldSource, - uint256 _maxYieldAmount, PassportBuilderScore _passportBuilderScore ) ERC4626(_token) ERC20("TalentProtocolVaultToken", "TALENTVAULT") Ownable(msg.sender) { if ( @@ -113,16 +107,24 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { } token = _token; - yieldRateBase = 0; + yieldRateBase = 5_00; yieldSource = _yieldSource; yieldRewardsFlag = true; - maxYieldAmount = _maxYieldAmount; + yieldAccrualDeadline = block.timestamp + 90 days; passportBuilderScore = _passportBuilderScore; - lockPeriod = 7 days; + lockPeriod = 30 days; + maxOverallDeposit = 1_000_000 ether; } // ------------------- EXTERNAL -------------------------------------------- + /// @notice Set the maximum amount of tokens that can be deposited into the vault + /// @dev Can only be called by the owner + /// @param _maxOverallDeposit The new maximum amount of tokens that can be deposited into the vault + function setMaxOverallDeposit(uint256 _maxOverallDeposit) external onlyOwner { + maxOverallDeposit = _maxOverallDeposit; + } + /// @notice Set the lock period for the contract /// @dev Can only be called by the owner /// @param _lockPeriod The lock period in days @@ -166,15 +168,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { emit YieldRateUpdated(_yieldRate); } - /// @notice Update the maximum amount of tokens that can be used to calculate rewards - /// @dev Can only be called by the owner - /// @param _maxYieldAmount The new maximum yield amount - function setMaxYieldAmount(uint256 _maxYieldAmount) external onlyOwner { - maxYieldAmount = _maxYieldAmount; - - emit MaxYieldAmountUpdated(_maxYieldAmount); - } - /// @notice Update the time at which the users of the contract will stop accruing rewards /// @dev Can only be called by the owner /// @param _yieldAccrualDeadline The new yield accrual deadline @@ -231,6 +224,10 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { revert InvalidDepositAmount(); } + if (totalAssets() + assets > maxOverallDeposit) { + revert MaxOverallDepositReached(); + } + refreshForAddress(receiver); uint256 shares = super.deposit(assets, receiver); @@ -271,10 +268,8 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { uint256 passportId = passportBuilderScore.passportRegistry().passportId(user); uint256 builderScore = passportBuilderScore.getScore(passportId); - if (builderScore < 50) return yieldRateBase; - if (builderScore < 75) return yieldRateBase + 5_00; - if (builderScore < 100) return yieldRateBase + 10_00; - return yieldRateBase + 15_00; + if (builderScore < 60) return yieldRateBase; + return yieldRateBase + 5_00; } /// @notice Prevents the owner from renouncing ownership @@ -312,10 +307,6 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { uint256 userBalance = balanceOf(user); - if (userBalance > maxYieldAmount) { - userBalance = maxYieldAmount; - } - uint256 endTime; if (yieldAccrualDeadline != 0 && block.timestamp > yieldAccrualDeadline) { diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 611b57d5..3f9486aa 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -13,7 +13,7 @@ const { expect } = chai; const { deployContract } = waffle; async function ensureTimeIsAfterLockPeriod() { - const lockPeriod = 8; + const lockPeriod = 31; const oneDayAfterLockPeriod = Math.floor(Date.now() / 1000) + lockPeriod * 24 * 60 * 60; await ensureTimestamp(oneDayAfterLockPeriod); } @@ -32,6 +32,7 @@ describe("TalentVault", () => { let snapshotId: bigint; let currentDateEpochSeconds: number; + const yieldBasePerDay = ethers.utils.parseEther("0.137"); before(async () => { await ethers.provider.send("hardhat_reset", []); @@ -49,7 +50,6 @@ describe("TalentVault", () => { talentVault = (await deployContract(admin, Artifacts.TalentVault, [ talentToken.address, yieldSource.address, - ethers.utils.parseEther("10000"), passportBuilderScore.address, ])) as TalentVault; @@ -94,16 +94,14 @@ describe("TalentVault", () => { }); it("Should set the correct initial values", async () => { - expect(await talentVault.yieldRateBase()).to.equal(0); - - expect(await talentVault.maxYieldAmount()).to.equal(ethers.utils.parseEther("10000")); + expect(await talentVault.yieldRateBase()).to.equal(5_00); expect(await talentVault.passportBuilderScore()).not.to.equal(ethers.constants.AddressZero); expect(await talentVault.passportBuilderScore()).to.equal(passportBuilderScore.address); expect(await talentVault.yieldRewardsFlag()).to.equal(true); - expect(await talentVault.lockPeriod()).to.equal(7 * 24 * 60 * 60); + expect(await talentVault.lockPeriod()).to.equal(30 * 24 * 60 * 60); }); it("reverts with InvalidAddress when _token given is 0", async () => { @@ -673,10 +671,10 @@ describe("TalentVault", () => { // Simulate time passing ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - const yieldedRewards = depositAmount.mul(0).div(100); // 0% rewards + const yieldedRewards = yieldBasePerDay.mul(90); // 5% rewards but over 90 days // this is manually calculated, but it is necessary for this test. - const expectedUser1TalentVaultBalanceAfter1Year = ethers.utils.parseEther("1000"); // there are no rewards + const expectedUser1TalentVaultBalanceAfter1Year = depositAmount.add(yieldedRewards); // fire await talentVault.connect(user1).withdrawAll(); @@ -690,7 +688,7 @@ describe("TalentVault", () => { const user1TalentBalanceAfter = await talentToken.balanceOf(user1.address); expect(user1TalentBalanceAfter).to.be.closeTo( expectedUser1TalentVaultBalanceAfter1Year, - ethers.utils.parseEther("0.001") + ethers.utils.parseEther("0.01") ); // user1 $TALENTVAULT balance goes to 0 @@ -702,7 +700,7 @@ describe("TalentVault", () => { const expectedYieldSourceTalentBalanceAfter = yieldSourceTalentBalanceBefore.sub(yieldedRewards); expect(yieldSourceTalentBalanceAfter).to.be.closeTo( expectedYieldSourceTalentBalanceAfter, - ethers.utils.parseEther("0.001") + ethers.utils.parseEther("0.01") ); }); }); @@ -717,13 +715,13 @@ describe("TalentVault", () => { // Simulate time passing ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - const expectedRewards = depositAmount.mul(0).div(100); // 0% rewards + const expectedRewards = yieldBasePerDay.mul(90); // 5% rewards but over 90 days // fire await talentVault.connect(user1).refresh(); const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.be.closeTo(depositAmount.add(expectedRewards), ethers.utils.parseEther("0.001")); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedRewards), ethers.utils.parseEther("0.01")); const userLastRewardCalculation = (await talentVault.userBalanceMeta(user1.address)).lastRewardCalculation; const oneYearAfterEpochSeconds = currentDateEpochSeconds + 31536000; @@ -758,27 +756,7 @@ describe("TalentVault", () => { }); }); - // 10000 - it("Should calculate rewards even if amount is above the max yield amount correctly", async () => { - const depositAmount = ethers.utils.parseEther("15000"); - const maxAmount = ethers.utils.parseEther("10000"); - await talentToken.transfer(user1.address, depositAmount); - await talentToken.connect(user1).approve(talentVault.address, depositAmount); - await talentVault.connect(user1).deposit(depositAmount, user1.address); - - // Simulate time passing - ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - - const expectedRewards = maxAmount.mul(0).div(100); // 0% rewards - - // fire - await talentVault.connect(user1).refresh(); - - const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.be.closeTo(depositAmount.add(expectedRewards), ethers.utils.parseEther("0.001")); - }); - - it("Should calculate rewards correctly for builders with scores above 50 but below 75", async () => { + it("Should calculate rewards correctly for builders with scores below 60", async () => { await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode await passportRegistry.connect(user1).create("source1"); @@ -795,38 +773,17 @@ describe("TalentVault", () => { // fire await talentVault.connect(user1).refresh(); - const expectedRewards = depositAmount.mul(5).div(100); // 5% rewards - const userBalance = await talentVault.balanceOf(user1.address); - expect(userBalance).to.be.closeTo(depositAmount.add(expectedRewards), ethers.utils.parseEther("0.1")); - }); - - it("Should calculate rewards correctly for builders with scores above 75 but below 100", async () => { - await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode - await passportRegistry.connect(user1).create("source1"); - - const passportId = await passportRegistry.passportId(user1.address); - const depositAmount = ethers.utils.parseEther("1000"); - await talentToken.transfer(user1.address, depositAmount); - await talentToken.connect(user1).approve(talentVault.address, depositAmount); - await talentVault.connect(user1).deposit(depositAmount, user1.address); - - // Simulate time passing - ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - await passportBuilderScore.setScore(passportId, 80); // Set builder score above 50 - - // fire - await talentVault.connect(user1).refresh(); - - const expectedRewards = depositAmount.mul(10).div(100); // 10% rewards + const expectedRewards = yieldBasePerDay.mul(90); // 5% rewards but over 90 days const userBalance = await talentVault.balanceOf(user1.address); expect(userBalance).to.be.closeTo(depositAmount.add(expectedRewards), ethers.utils.parseEther("0.1")); }); - it("Should calculate rewards correctly for builders with scores above 100", async () => { + it("Should calculate rewards correctly for builders with scores above 60 (inclusive)", async () => { await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode await passportRegistry.connect(user1).create("source1"); const passportId = await passportRegistry.passportId(user1.address); + await passportBuilderScore.setScore(passportId, 60); // Set builder score above 60 const depositAmount = ethers.utils.parseEther("1000"); await talentToken.transfer(user1.address, depositAmount); await talentToken.connect(user1).approve(talentVault.address, depositAmount); @@ -834,12 +791,12 @@ describe("TalentVault", () => { // Simulate time passing ensureTimestamp(currentDateEpochSeconds + 31536000); // 1 year ahead - await passportBuilderScore.setScore(passportId, 105); // Set builder score above 75 + await passportBuilderScore.setScore(passportId, 60); // Set builder score above 60 // fire await talentVault.connect(user1).refresh(); - const expectedRewards = depositAmount.mul(15).div(100); // 15% rewards + const expectedRewards = yieldBasePerDay.mul(2).mul(90); // 10% rewards but over 90 days const userBalance = await talentVault.balanceOf(user1.address); expect(userBalance).to.be.closeTo(depositAmount.add(expectedRewards), ethers.utils.parseEther("0.1")); }); From 1cb716342298f9c6d20d6d5d54b8545d9e023d4f Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Fri, 17 Jan 2025 17:16:28 +0000 Subject: [PATCH 72/74] Update deploy vault script --- contracts/talent/TalentVault.sol | 2 +- scripts/shared/index.ts | 8 +------- scripts/talent/deployTalentVault.ts | 10 ++++------ 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index b3c15a71..0169166f 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -97,7 +97,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { IERC20 _token, address _yieldSource, PassportBuilderScore _passportBuilderScore - ) ERC4626(_token) ERC20("TalentProtocolVaultToken", "TALENTVAULT") Ownable(msg.sender) { + ) ERC4626(_token) ERC20("TalentVault", "sTALENT") Ownable(msg.sender) { if ( address(_token) == address(0) || address(_yieldSource) == address(0) || diff --git a/scripts/shared/index.ts b/scripts/shared/index.ts index ffb10276..2a7a99a8 100644 --- a/scripts/shared/index.ts +++ b/scripts/shared/index.ts @@ -150,17 +150,11 @@ export async function deployTalentTGEUnlockTimestamps( export async function deployTalentVault( talentToken: string, yieldSource: string, - maxYieldAmount: BigNumber, passportBuilderScore: string ): Promise { const talentVaultContract = await ethers.getContractFactory("TalentVault"); - const deployedTalentVault = await talentVaultContract.deploy( - talentToken, - yieldSource, - maxYieldAmount, - passportBuilderScore - ); + const deployedTalentVault = await talentVaultContract.deploy(talentToken, yieldSource, passportBuilderScore); await deployedTalentVault.deployed(); return deployedTalentVault as TalentVault; diff --git a/scripts/talent/deployTalentVault.ts b/scripts/talent/deployTalentVault.ts index b4c507a0..96c1eb05 100644 --- a/scripts/talent/deployTalentVault.ts +++ b/scripts/talent/deployTalentVault.ts @@ -1,14 +1,13 @@ import { ethers, network } from "hardhat"; -import { BigNumber } from "ethers"; import { deployTalentVault } from "../shared"; -const TALENT_TOKEN_MAINNET = ""; +const TALENT_TOKEN_MAINNET = "0x9a33406165f562E16C3abD82fd1185482E01b49a"; const TALENT_TOKEN_TESTNET = "0x7c2a63e1713578d4d704b462C2dee311A59aE304"; const YIELD_SOURCE_MAINNET = ""; const YIELD_SOURCE_TESTNET = "0x33041027dd8F4dC82B6e825FB37ADf8f15d44053"; -const PASSPORT_BUILDER_SCORE_MAINNET = ""; +const PASSPORT_BUILDER_SCORE_MAINNET = "0xBBFeDA7c4d8d9Df752542b03CdD715F790B32D0B"; const PASSPORT_BUILDER_SCORE_TESTNET = "0x5f3aA689C4DCBAe505E6F6c8548DbD9b908bA71d"; async function main() { @@ -18,12 +17,9 @@ async function main() { console.log(`Admin will be ${admin.address}`); - const maxYieldAmount = ethers.utils.parseEther("10000"); - const talentVault = await deployTalentVault( TALENT_TOKEN_TESTNET, YIELD_SOURCE_TESTNET, - maxYieldAmount, PASSPORT_BUILDER_SCORE_TESTNET ); @@ -32,6 +28,8 @@ async function main() { `Params for verification: Contract ${talentVault.address} Owner ${admin.address} Talent Token ${TALENT_TOKEN_TESTNET} Yield Source ${YIELD_SOURCE_TESTNET}` ); + console.log("Approve the vault to spend the talent tokens: ", ethers.utils.parseEther("100000")); + console.log("Done"); } From e7e16288f19b3e9884e25170e874452feb115645 Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Fri, 17 Jan 2025 17:29:50 +0000 Subject: [PATCH 73/74] Add coverage for max amount in vault --- test/contracts/talent/TalentVault.ts | 72 +++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 3f9486aa..3cef65b1 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -46,7 +46,7 @@ describe("TalentVault", () => { admin.address, ])) as PassportBuilderScore; - const adminInitialDeposit = ethers.utils.parseEther("200000"); + // const adminInitialDeposit = ethers.utils.parseEther("200000"); talentVault = (await deployContract(admin, Artifacts.TalentVault, [ talentToken.address, yieldSource.address, @@ -71,7 +71,7 @@ describe("TalentVault", () => { // just make sure that TV wallet has $TALENT as initial assets from admin initial deposit await talentToken.approve(talentVault.address, ethers.constants.MaxUint256); - await talentVault.mint(adminInitialDeposit, admin.address); + // await talentVault.mint(adminInitialDeposit, admin.address); // fund the yieldSource with lots of TALENT Balance await talentToken.transfer(yieldSource.address, ethers.utils.parseEther("100000")); @@ -139,18 +139,18 @@ describe("TalentVault", () => { }); describe("#name", async () => { - it("is 'TalentProtocolVaultToken' reflects the underlying token name, i.e. of 'TalentProtocolToken'", async () => { + it("is 'TalentVault' reflects the underlying token name, i.e. of 'TalentProtocolToken'", async () => { const name = await talentVault.name(); - expect(name).to.equal("TalentProtocolVaultToken"); + expect(name).to.equal("TalentVault"); }); }); describe("#symbol", async () => { - it("is 'TALENTVAULT' reflects the underlying token symbol, i.e. of 'TALENT'", async () => { + it("is 'sTALENT' reflects the underlying token symbol, i.e. of 'TALENT'", async () => { const symbol = await talentVault.symbol(); - expect(symbol).to.equal("TALENTVAULT"); + expect(symbol).to.equal("sTALENT"); }); }); @@ -272,6 +272,48 @@ describe("TalentVault", () => { await expect(talentVault.connect(user1).deposit(0n, user1.address)).to.be.revertedWith("InvalidDepositAmount"); }); + it("Should revert if $TALENT deposited is greater than the max overall deposit", async () => { + await talentVault.setMaxOverallDeposit(ethers.utils.parseEther("100000")); + await expect( + talentVault.connect(user1).deposit(ethers.utils.parseEther("100001"), user1.address) + ).to.be.revertedWith("MaxOverallDepositReached"); + }); + + it("Should allow deposit of amount equal to the max overall deposit", async () => { + const maxOverallDeposit = ethers.utils.parseEther("100000"); + const totalAssetsBefore = await talentVault.totalAssets(); + + await talentToken.transfer(user1.address, maxOverallDeposit.sub(totalAssetsBefore)); + await talentToken.connect(user1).approve(talentVault.address, maxOverallDeposit.sub(totalAssetsBefore)); + await talentVault.setMaxOverallDeposit(maxOverallDeposit.sub(totalAssetsBefore)); + await talentVault.connect(user1).deposit(maxOverallDeposit.sub(totalAssetsBefore), user1.address); + + expect(await talentVault.totalAssets()).to.equal(maxOverallDeposit); + }); + + it("Should allow deposit of amount greater than the max overall deposit if its increased", async () => { + const maxOverallDeposit = ethers.utils.parseEther("100000"); + const totalAssetsBefore = await talentVault.totalAssets(); + + await talentToken.transfer(user1.address, maxOverallDeposit.sub(totalAssetsBefore)); + await talentToken.connect(user1).approve(talentVault.address, maxOverallDeposit.sub(totalAssetsBefore)); + await talentVault.setMaxOverallDeposit(maxOverallDeposit.sub(totalAssetsBefore)); + await talentVault.connect(user1).deposit(maxOverallDeposit.sub(totalAssetsBefore), user1.address); + + expect(await talentVault.totalAssets()).to.equal(maxOverallDeposit); + + const nextDepositAmount = ethers.utils.parseEther("1"); + await talentVault.setMaxOverallDeposit(maxOverallDeposit.add(nextDepositAmount)); + await talentToken.transfer(user1.address, nextDepositAmount); + await talentToken.connect(user1).approve(talentVault.address, nextDepositAmount); + await talentVault.connect(user1).deposit(nextDepositAmount, user1.address); + + expect(await talentVault.totalAssets()).to.be.closeTo( + maxOverallDeposit.add(nextDepositAmount), + ethers.utils.parseEther("0.01") + ); + }); + it("Should not allow deposit of amount that the sender does not have", async () => { const balanceOfUser1 = 100_000n; @@ -835,6 +877,24 @@ describe("TalentVault", () => { }); }); + describe("#maxOverallDeposit", async () => { + context("when called by an non-owner account", async () => { + it("reverts", async () => { + await expect( + talentVault.connect(user1).setMaxOverallDeposit(ethers.utils.parseEther("100000")) + ).to.be.revertedWith(`OwnableUnauthorizedAccount("${user1.address}")`); + }); + }); + + context("when called by the owner account", async () => { + it("sets the max overall deposit", async () => { + await talentVault.setMaxOverallDeposit(ethers.utils.parseEther("500000")); + + expect(await talentVault.maxOverallDeposit()).to.equal(ethers.utils.parseEther("500000")); + }); + }); + }); + describe("#startYieldingRewards", async () => { context("when called by an non-owner account", async () => { it("reverts", async () => { From 3fc5b993f97ffdf08a396e377e8ffb43086ed44e Mon Sep 17 00:00:00 2001 From: Francisco Leal Date: Sat, 18 Jan 2025 16:56:09 +0000 Subject: [PATCH 74/74] Update vault with wallet registry --- contracts/talent/TalentVault.sol | 11 ++++++++--- scripts/shared/index.ts | 14 +++++++++----- scripts/talent/deployTalentVault.ts | 13 ++++++++----- test/contracts/talent/TalentVault.ts | 14 +++++++++++++- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/contracts/talent/TalentVault.sol b/contracts/talent/TalentVault.sol index 0169166f..63718dc2 100644 --- a/contracts/talent/TalentVault.sol +++ b/contracts/talent/TalentVault.sol @@ -7,6 +7,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "../passport/PassportBuilderScore.sol"; +import "../passport/PassportWalletRegistry.sol"; /// @title Talent Protocol Vault Token Contract /// @author Talent Protocol - Francisco Leal, Panagiotis Matsinopoulos @@ -80,6 +81,9 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice The Passport Builder Score contract PassportBuilderScore public passportBuilderScore; + /// @notice The Passport Wallet Registry contract + PassportWalletRegistry public passportWalletRegistry; + /// @notice A mapping of user addresses to their deposits mapping(address => UserBalanceMeta) public userBalanceMeta; @@ -96,7 +100,8 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { constructor( IERC20 _token, address _yieldSource, - PassportBuilderScore _passportBuilderScore + PassportBuilderScore _passportBuilderScore, + PassportWalletRegistry _passportWalletRegistry ) ERC4626(_token) ERC20("TalentVault", "sTALENT") Ownable(msg.sender) { if ( address(_token) == address(0) || @@ -114,6 +119,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { passportBuilderScore = _passportBuilderScore; lockPeriod = 30 days; maxOverallDeposit = 1_000_000 ether; + passportWalletRegistry = _passportWalletRegistry; } // ------------------- EXTERNAL -------------------------------------------- @@ -264,8 +270,7 @@ contract TalentVault is ERC4626, Ownable, ReentrancyGuard { /// @notice Get the yield rate for the contract for a given user /// @param user The address of the user to get the yield rate for function getYieldRateForScore(address user) public view returns (uint256) { - /// @TODO: Update to use the PassportWalletRegistry instead for calculating the passport id - uint256 passportId = passportBuilderScore.passportRegistry().passportId(user); + uint256 passportId = passportWalletRegistry.passportId(user); uint256 builderScore = passportBuilderScore.getScore(passportId); if (builderScore < 60) return yieldRateBase; diff --git a/scripts/shared/index.ts b/scripts/shared/index.ts index 2a7a99a8..6f75a116 100644 --- a/scripts/shared/index.ts +++ b/scripts/shared/index.ts @@ -1,5 +1,4 @@ import { ethers } from "hardhat"; -import { zeroHash } from "viem"; import type { PassportRegistry, TalentProtocolToken, @@ -12,8 +11,6 @@ import type { TalentTGEUnlockTimestamp, TalentVault, } from "../../typechain-types"; -import { BigNumber } from "ethers"; -import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; export async function deployPassport(owner: string): Promise { const passportRegistryContract = await ethers.getContractFactory("PassportRegistry"); @@ -150,11 +147,18 @@ export async function deployTalentTGEUnlockTimestamps( export async function deployTalentVault( talentToken: string, yieldSource: string, - passportBuilderScore: string + passportBuilderScore: string, + passportWalletRegistry: string ): Promise { const talentVaultContract = await ethers.getContractFactory("TalentVault"); - const deployedTalentVault = await talentVaultContract.deploy(talentToken, yieldSource, passportBuilderScore); + const deployedTalentVault = await talentVaultContract.deploy( + talentToken, + yieldSource, + passportBuilderScore, + passportWalletRegistry + ); + await deployedTalentVault.deployed(); return deployedTalentVault as TalentVault; diff --git a/scripts/talent/deployTalentVault.ts b/scripts/talent/deployTalentVault.ts index 96c1eb05..6072180c 100644 --- a/scripts/talent/deployTalentVault.ts +++ b/scripts/talent/deployTalentVault.ts @@ -4,12 +4,14 @@ import { deployTalentVault } from "../shared"; const TALENT_TOKEN_MAINNET = "0x9a33406165f562E16C3abD82fd1185482E01b49a"; const TALENT_TOKEN_TESTNET = "0x7c2a63e1713578d4d704b462C2dee311A59aE304"; -const YIELD_SOURCE_MAINNET = ""; +const YIELD_SOURCE_MAINNET = "0x34118871f5943D6B153381C0133115c3B5b78b12"; const YIELD_SOURCE_TESTNET = "0x33041027dd8F4dC82B6e825FB37ADf8f15d44053"; const PASSPORT_BUILDER_SCORE_MAINNET = "0xBBFeDA7c4d8d9Df752542b03CdD715F790B32D0B"; const PASSPORT_BUILDER_SCORE_TESTNET = "0x5f3aA689C4DCBAe505E6F6c8548DbD9b908bA71d"; +const WALLET_REGISTRY_MAINNET = "0x9B729d9fC43e3746855F7E02238FB3a2A20bD899"; + async function main() { console.log(`Deploying Talent Vault at ${network.name}`); @@ -18,14 +20,15 @@ async function main() { console.log(`Admin will be ${admin.address}`); const talentVault = await deployTalentVault( - TALENT_TOKEN_TESTNET, - YIELD_SOURCE_TESTNET, - PASSPORT_BUILDER_SCORE_TESTNET + TALENT_TOKEN_MAINNET, + YIELD_SOURCE_MAINNET, + PASSPORT_BUILDER_SCORE_MAINNET, + WALLET_REGISTRY_MAINNET ); console.log(`Talent Vault deployed at ${talentVault.address}`); console.log( - `Params for verification: Contract ${talentVault.address} Owner ${admin.address} Talent Token ${TALENT_TOKEN_TESTNET} Yield Source ${YIELD_SOURCE_TESTNET}` + `Params for verification: Contract ${talentVault.address} Owner ${admin.address} Talent Token ${TALENT_TOKEN_MAINNET} Yield Source ${YIELD_SOURCE_MAINNET} Wallet Registry ${WALLET_REGISTRY_MAINNET}` ); console.log("Approve the vault to spend the talent tokens: ", ethers.utils.parseEther("100000")); diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 3cef65b1..2a04c30d 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -3,7 +3,13 @@ import { ethers, waffle } from "hardhat"; import { solidity } from "ethereum-waffle"; import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { TalentProtocolToken, TalentVault, PassportRegistry, PassportBuilderScore } from "../../../typechain-types"; +import { + TalentProtocolToken, + TalentVault, + PassportRegistry, + PassportBuilderScore, + PassportWalletRegistry, +} from "../../../typechain-types"; import { Artifacts } from "../../shared"; import { ensureTimestamp } from "../../shared/utils"; @@ -27,6 +33,7 @@ describe("TalentVault", () => { let talentToken: TalentProtocolToken; let passportRegistry: PassportRegistry; + let passportWalletRegistry: PassportWalletRegistry; let passportBuilderScore: PassportBuilderScore; let talentVault: TalentVault; @@ -41,6 +48,10 @@ describe("TalentVault", () => { talentToken = (await deployContract(admin, Artifacts.TalentProtocolToken, [admin.address])) as TalentProtocolToken; passportRegistry = (await deployContract(admin, Artifacts.PassportRegistry, [admin.address])) as PassportRegistry; + passportWalletRegistry = (await deployContract(admin, Artifacts.PassportWalletRegistry, [ + admin.address, + passportRegistry.address, + ])) as PassportWalletRegistry; passportBuilderScore = (await deployContract(admin, Artifacts.PassportBuilderScore, [ passportRegistry.address, admin.address, @@ -51,6 +62,7 @@ describe("TalentVault", () => { talentToken.address, yieldSource.address, passportBuilderScore.address, + passportWalletRegistry.address, ])) as TalentVault; console.log("------------------------------------");