diff --git a/.tool-versions b/.tool-versions index c6743f9b..4d6a5199 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,3 @@ nodejs 20.12.2 solidity 0.8.24 +yarn 1.22.19 diff --git a/contracts/passport/FixedBuilderScore.sol b/contracts/passport/FixedBuilderScore.sol new file mode 100644 index 00000000..0a6cc39a --- /dev/null +++ b/contracts/passport/FixedBuilderScore.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +/// @title Fixed Builder Score +/// @notice A dummy PassportBuilderScore that returns a fixed score for all users +contract FixedBuilderScore is Ownable { + uint256 public fixedScore; + + event FixedScoreUpdated(uint256 oldScore, uint256 newScore); + + constructor(uint256 _fixedScore, address _owner) Ownable(_owner) { + fixedScore = _fixedScore; + } + + /// @notice Set the fixed score returned for all passport IDs + /// @param _fixedScore The new fixed score + function setFixedScore(uint256 _fixedScore) external onlyOwner { + uint256 oldScore = fixedScore; + fixedScore = _fixedScore; + emit FixedScoreUpdated(oldScore, _fixedScore); + } + + /// @notice Returns the fixed score for any passport ID + /// @param _passportId Ignored - returns the same score for all + /// @return The fixed score + function getScore(uint256 _passportId) external view returns (uint256) { + return fixedScore; + } +} diff --git a/scripts/passport/deployFixedBuilderScore.ts b/scripts/passport/deployFixedBuilderScore.ts new file mode 100644 index 00000000..e13a1143 --- /dev/null +++ b/scripts/passport/deployFixedBuilderScore.ts @@ -0,0 +1,53 @@ +import { ethers, network } from "hardhat"; + +const TALENT_VAULT_MAINNET = "0x23Ff3256A29847d7EF760943bd6679b565CbdE5a"; + +// Set to 60 to give everyone the bonus yield rate, or 0 for base rate only +const INITIAL_FIXED_SCORE = 60; + +async function main() { + const isMainnet = network.name === "mainnet" || network.name === "base"; + + if (!isMainnet) { + return; + } + + const talentVaultAddress = TALENT_VAULT_MAINNET; + + console.log(`Deploying Fixed Builder Score at ${network.name}`); + + const [admin] = await ethers.getSigners(); + + console.log(`Admin/Owner will be ${admin.address}`); + + // Deploy FixedBuilderScore + const fixedBuilderScoreContract = await ethers.getContractFactory("FixedBuilderScore"); + const fixedBuilderScore = await fixedBuilderScoreContract.deploy(INITIAL_FIXED_SCORE, admin.address); + await fixedBuilderScore.deployed(); + + console.log(`Fixed Builder Score deployed at ${fixedBuilderScore.address}`); + console.log(`Initial fixed score: ${INITIAL_FIXED_SCORE}`); + console.log(`Owner: ${admin.address}`); + + // Update TalentVault to use the new FixedBuilderScore + console.log(`\nUpdating TalentVault at ${talentVaultAddress}...`); + const talentVault = await ethers.getContractAt("TalentVault", talentVaultAddress); + const tx = await talentVault.setPassportBuilderScore(fixedBuilderScore.address); + await tx.wait(); + + console.log(`TalentVault.setPassportBuilderScore updated to ${fixedBuilderScore.address}`); + + console.log("\n--- Verification Command ---"); + console.log( + `npx hardhat verify --network ${network.name} ${fixedBuilderScore.address} ${INITIAL_FIXED_SCORE} ${admin.address}` + ); + + console.log("\nDone"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/test/contracts/talent/TalentVault.ts b/test/contracts/talent/TalentVault.ts index 2a04c30d..c801dc69 100644 --- a/test/contracts/talent/TalentVault.ts +++ b/test/contracts/talent/TalentVault.ts @@ -9,6 +9,7 @@ import { PassportRegistry, PassportBuilderScore, PassportWalletRegistry, + FixedBuilderScore, } from "../../../typechain-types"; import { Artifacts } from "../../shared"; import { ensureTimestamp } from "../../shared/utils"; @@ -942,4 +943,128 @@ describe("TalentVault", () => { }); }); }); + + describe("FixedBuilderScore", async () => { + let fixedBuilderScore: FixedBuilderScore; + + beforeEach(async () => { + fixedBuilderScore = (await deployContract(admin, Artifacts.FixedBuilderScore, [ + 60, + admin.address, + ])) as FixedBuilderScore; + }); + + describe("Deployment", async () => { + it("Should set the correct owner", async () => { + expect(await fixedBuilderScore.owner()).to.equal(admin.address); + }); + + it("Should set the correct initial fixed score", async () => { + expect(await fixedBuilderScore.fixedScore()).to.equal(60); + }); + }); + + describe("#getScore", async () => { + it("returns the fixed score for any passport ID", async () => { + expect(await fixedBuilderScore.getScore(1)).to.equal(60); + expect(await fixedBuilderScore.getScore(999)).to.equal(60); + expect(await fixedBuilderScore.getScore(0)).to.equal(60); + }); + }); + + describe("#setFixedScore", async () => { + context("when called by the owner", async () => { + it("updates the fixed score", async () => { + await fixedBuilderScore.setFixedScore(80); + expect(await fixedBuilderScore.fixedScore()).to.equal(80); + }); + + it("emits FixedScoreUpdated event", async () => { + await expect(fixedBuilderScore.setFixedScore(80)) + .to.emit(fixedBuilderScore, "FixedScoreUpdated") + .withArgs(60, 80); + }); + }); + + context("when called by a non-owner", async () => { + it("reverts", async () => { + await expect(fixedBuilderScore.connect(user1).setFixedScore(80)).to.be.revertedWith( + `OwnableUnauthorizedAccount("${user1.address}")` + ); + }); + }); + }); + + describe("TalentVault with FixedBuilderScore", async () => { + beforeEach(async () => { + // Swap PassportBuilderScore for FixedBuilderScore in TalentVault + await talentVault.setPassportBuilderScore(fixedBuilderScore.address); + }); + + it("uses the fixed score for yield calculation", async () => { + expect(await talentVault.passportBuilderScore()).to.equal(fixedBuilderScore.address); + }); + + context("when fixed score is >= 60 (bonus tier)", async () => { + it("calculates rewards at 10% APY", async () => { + await fixedBuilderScore.setFixedScore(60); + + 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 talentVault.connect(user1).refresh(); + + // 10% yield over 90 days (yieldAccrualDeadline) + const expectedRewards = yieldBasePerDay.mul(2).mul(90); + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedRewards), ethers.utils.parseEther("0.1")); + }); + }); + + context("when fixed score is < 60 (base tier)", async () => { + it("calculates rewards at 5% APY", async () => { + await fixedBuilderScore.setFixedScore(59); + + 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 talentVault.connect(user1).refresh(); + + // 5% yield over 90 days (yieldAccrualDeadline) + const expectedRewards = yieldBasePerDay.mul(90); + const userBalance = await talentVault.balanceOf(user1.address); + expect(userBalance).to.be.closeTo(depositAmount.add(expectedRewards), ethers.utils.parseEther("0.1")); + }); + }); + + context("when admin changes fixed score", async () => { + it("affects future yield calculations for all users", async () => { + // Start with score 0 (base rate) + await fixedBuilderScore.setFixedScore(0); + + 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); + + // Change to bonus tier + await fixedBuilderScore.setFixedScore(60); + + // All users now get the bonus rate + const yieldRate = await talentVault.getYieldRateForScore(user1.address); + expect(yieldRate).to.equal(10_00); // 10% + }); + }); + }); + }); }); diff --git a/test/shared/artifacts.ts b/test/shared/artifacts.ts index 3e28f522..8dffd9fe 100644 --- a/test/shared/artifacts.ts +++ b/test/shared/artifacts.ts @@ -15,6 +15,7 @@ import TalentVault from "../../artifacts/contracts/talent/TalentVault.sol/Talent import TalentVaultV2 from "../../artifacts/contracts/talent/TalentVaultV2.sol/TalentVaultV2.json"; import BaseAPY from "../../artifacts/contracts/talent/vault-options/BaseAPY.sol/BaseAPY.json"; import MultiSendETH from "../../artifacts/contracts/utils/MultiSendETH.sol/MultiSendETH.json"; +import FixedBuilderScore from "../../artifacts/contracts/passport/FixedBuilderScore.sol/FixedBuilderScore.json"; export { PassportRegistry, @@ -34,4 +35,5 @@ export { TalentVaultV2, BaseAPY, MultiSendETH, + FixedBuilderScore, };