Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
nodejs 20.12.2
solidity 0.8.24
yarn 1.22.19
31 changes: 31 additions & 0 deletions contracts/passport/FixedBuilderScore.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
53 changes: 53 additions & 0 deletions scripts/passport/deployFixedBuilderScore.ts
Original file line number Diff line number Diff line change
@@ -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);
});
125 changes: 125 additions & 0 deletions test/contracts/talent/TalentVault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
PassportRegistry,
PassportBuilderScore,
PassportWalletRegistry,
FixedBuilderScore,
} from "../../../typechain-types";
import { Artifacts } from "../../shared";
import { ensureTimestamp } from "../../shared/utils";
Expand Down Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing await on async ensureTimestamp calls

Medium Severity

The ensureTimestamp function returns a Promise and needs to be awaited. Without await, the EVM timestamp may not be set before the subsequent refresh() call executes, causing the test to calculate rewards over an incorrect time period. Other usages in TalentRewardClaim.ts and the ensureTimeIsAfterLockPeriod helper function correctly use await.

Additional Locations (1)

Fix in Cursor Fix in Web


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%
});
});
});
});
});
2 changes: 2 additions & 0 deletions test/shared/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -34,4 +35,5 @@ export {
TalentVaultV2,
BaseAPY,
MultiSendETH,
FixedBuilderScore,
};
Loading