Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TGE Contracts #85

Merged
merged 10 commits into from
May 27, 2024
52 changes: 52 additions & 0 deletions contracts/passport/PassportBuilderScore.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/access/Ownable.sol";
import "./PassportRegistry.sol";

contract PassportBuilderScore is Ownable {
PassportRegistry public passportRegistry;

// Mapping to store scores for each passport ID
mapping(uint256 => uint256) private passportScores;

event ScoreUpdated(uint256 indexed passportId, uint256 score);
event PassportRegistryChanged(address indexed oldAddress, address indexed newAddress);

constructor(address passportRegistryAddress, address initialOwner) Ownable(initialOwner) {
passportRegistry = PassportRegistry(passportRegistryAddress);
}

/**
* @notice Sets the score for a given passport ID.
* @dev Can only be called by the owner.
* @param passportId The ID of the passport to set the score for.
* @param score The score to set for the passport ID.
*/
function setScore(uint256 passportId, uint256 score) external onlyOwner {
require(passportRegistry.idPassport(passportId) != address(0), "Passport ID does not exist");
passportScores[passportId] = score;
emit ScoreUpdated(passportId, score);
}

/**
* @notice Gets the score of a given passport ID.
* @param passportId The ID of the passport to get the score for.
* @return The score of the given passport ID.
*/
function getScore(uint256 passportId) external view returns (uint256) {
return passportScores[passportId];
}

/**
* @notice Changes the address of the PassportRegistry contract.
* @dev Can only be called by the owner.
* @param newPassportRegistryAddress The address of the new PassportRegistry contract.
*/
function setPassportRegistry(address newPassportRegistryAddress) external onlyOwner {
require(newPassportRegistryAddress != address(0), "Invalid address");
address oldAddress = address(passportRegistry);
passportRegistry = PassportRegistry(newPassportRegistryAddress);
emit PassportRegistryChanged(oldAddress, newPassportRegistryAddress);
}
}
40 changes: 40 additions & 0 deletions contracts/talent/TalentProtocolToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

// Import OpenZeppelin contracts
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";

contract TalentProtocolToken is ERC20, ERC20Burnable, Pausable, ERC20Permit, Ownable {
// Mint 1B tokens to the initial owner and pause the contract
constructor(address initialOwner)
ERC20("TalentProtocolToken", "TALENT")
ERC20Permit("TalentProtocolToken")
Ownable(initialOwner)
{
_mint(initialOwner, 1_000_000_000 ether);
_pause();
}

function _update(address from, address to, uint256 value) internal virtual override(ERC20) {
francisco-leal marked this conversation as resolved.
Show resolved Hide resolved
require(to != address(this), "TalentProtocolToken: cannot transfer tokens to self");
require(!paused() || owner() == _msgSender(), "Token transfer is not enabled while paused");
super._update(from, to, value);
}

// Function to pause token transfers
function pause() external onlyOwner {
require(!paused(), "Token is already paused");
_pause();
}

// Function to unpause token transfers
function unpause() external onlyOwner {
require(paused(), "Token is not paused");
_unpause();
}
}
177 changes: 177 additions & 0 deletions contracts/talent/TalentRewardClaim.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./TalentProtocolToken.sol";
import "../passport/PassportBuilderScore.sol";

contract TalentRewardClaim is Ownable, ReentrancyGuard {
using Math for uint256;

TalentProtocolToken public talentToken;
PassportBuilderScore public passportBuilderScore;
address public holdingWallet;
uint256 public constant WEEKLY_CLAIM_AMOUNT = 2000 ether;
uint256 public constant WEEK_DURATION = 7 days;
uint256 public constant MAX_CLAIM_WEEKS = 104;
bool public setupComplete = false; // Setup flag
uint256 public startTime; // Track the start time

struct UserInfo {
uint256 amountOwed;
uint256 lastClaimed;
}

mapping(address => UserInfo) public userInfo;

event TokensClaimed(address indexed user, uint256 amount);
event TokensBurned(address indexed user, uint256 amount);
event SetupComplete();
event StartTimeSet(uint256 startTime);
event UserInitialized(address indexed user, uint256 amount, uint256 lastClaimed);

constructor(
TalentProtocolToken _talentToken,
PassportBuilderScore _passportBuilderScore,
address _holdingWallet,
address initialOwner
) Ownable(initialOwner) {
talentToken = _talentToken;
passportBuilderScore = _passportBuilderScore;
holdingWallet = _holdingWallet;
}

/**
* @notice Initializes the user information with the specified addresses and amounts.
* @dev Can only be called by the owner. This function sets up the initial state for each user
* with their corresponding amount owed. It also ensures that the number of users matches the
* number of amounts provided.
* @param users An array of addresses representing the users to initialize.
* @param amounts An array of uint256 values representing the amounts owed to each user.
*/
function initializeUsers(
address[] memory users,
uint256[] memory amounts,
uint256[] memory lastClaims
) external onlyOwner {
require(users.length == amounts.length, "Users and amounts length mismatch");
require(users.length == lastClaims.length, "Users and lastClaims length mismatch");

for (uint256 i = 0; i < users.length; i++) {
userInfo[users[i]] = UserInfo({
amountOwed: amounts[i],
lastClaimed: lastClaims[i]
});
francisco-leal marked this conversation as resolved.
Show resolved Hide resolved
emit UserInitialized(users[i], amounts[i], lastClaims[i]);
}
}

/**
* @notice Finalizes the setup process.
* @dev Can only be called by the owner. This function sets the setupComplete flag to true,
* indicating that the initialization process is complete and no further initialization can occur.
*/
function finalizeSetup() external onlyOwner {
setupComplete = true;
emit SetupComplete();
}

/**
* @notice Sets the start time for token claims.
* @dev Can only be called by the owner. This function initializes the startTime variable with the provided value.
* @param _startTime The timestamp representing the start time for token claims.
*/
function setStartTime(uint256 _startTime) external onlyOwner {
startTime = _startTime;
emit StartTimeSet(_startTime);
}

/**
* @notice Allows users to claim their owed tokens.
* @dev Can only be called once the setup is complete and the start time is set. This function calculates
* the number of weeks since the last claim and allows users to claim tokens based on their builder score.
* It also burns tokens for missed weeks if applicable.
* @dev Uses the nonReentrant modifier to prevent reentrancy attacks.
*/
function claimTokens() external nonReentrant {
require(setupComplete, "Setup is not complete");
require(startTime > 0, "Start time not set");

UserInfo storage user = userInfo[msg.sender];
require(user.amountOwed > 0, "No tokens owed");

uint256 passportId = passportBuilderScore.passportRegistry().passportId(msg.sender);
uint256 builderScore = passportBuilderScore.getScore(passportId);

uint256 claimMultiplier = (builderScore > 40) ? 5 : 1;
uint256 maxPerWeekAmountForUser = WEEKLY_CLAIM_AMOUNT * claimMultiplier;

// calculate number of weeks that have passed since start time
uint256 weeksPassed = (block.timestamp - startTime) / WEEK_DURATION;
uint256 weeksSinceLastClaim = 0;

if (user.lastClaimed != 0) {
weeksSinceLastClaim = (block.timestamp - user.lastClaimed) / WEEK_DURATION;
require(weeksSinceLastClaim > 0, "Can only claim once per week");
} else {
weeksSinceLastClaim = weeksPassed;
}

if (weeksPassed >= MAX_CLAIM_WEEKS) {
// Calculate the number of weeks missed
uint256 weeksMissed = 0;
if (user.lastClaimed != 0) {
weeksMissed = weeksPassed - weeksSinceLastClaim;
} else {
weeksMissed = weeksPassed;
}

// Burn the equivalent amount of tokens for the missed weeks
uint256 amountToBurn = Math.min(WEEKLY_CLAIM_AMOUNT * weeksMissed, user.amountOwed);
Copy link
Collaborator

Choose a reason for hiding this comment

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

weeksMissed here can be higher than MAX_CLAIM_WEEKS. Is it supposed to or should it be ceiled to 96?

Copy link
Member Author

Choose a reason for hiding this comment

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

the logic is correct, if the user claims after MAX_CLAIM_WEEKS + 10, then more tokens should be burned than if it was claimed at the MAX_CLAIM_WEEKS

user.amountOwed -= amountToBurn;

// Transfer the remaining owed amount to the user
uint256 amountToTransfer = user.amountOwed;
user.amountOwed = 0;
user.lastClaimed = block.timestamp;

if (amountToTransfer > 0) {
talentToken.transferFrom(holdingWallet, msg.sender, amountToTransfer);
emit TokensClaimed(msg.sender, amountToTransfer);
}

if (amountToBurn > 0) {
talentToken.burnFrom(holdingWallet, amountToBurn);
emit TokensBurned(msg.sender, amountToBurn);
}
} else {
uint256 amountToBurn = Math.min(WEEKLY_CLAIM_AMOUNT * (weeksSinceLastClaim - 1), user.amountOwed);
user.amountOwed -= amountToBurn;

uint256 amountToTransfer = Math.min(maxPerWeekAmountForUser, user.amountOwed);
user.amountOwed -= amountToTransfer;

user.lastClaimed = block.timestamp;

if (amountToTransfer > 0) {
talentToken.transferFrom(holdingWallet, msg.sender, amountToTransfer);
emit TokensClaimed(msg.sender, amountToTransfer);
}
if (amountToBurn > 0) {
talentToken.burnFrom(holdingWallet, amountToBurn);
emit TokensBurned(msg.sender, amountToBurn);
}
}
}

function tokensOwed(address user) external view returns (uint256) {
return userInfo[user].amountOwed;
}

function lastClaimed(address user) external view returns (uint256) {
return userInfo[user].lastClaimed;
}
}
124 changes: 124 additions & 0 deletions test/contracts/passport/PassportBuilderScore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import chai from "chai";
import { ethers, waffle } from "hardhat";
import { solidity } from "ethereum-waffle";

import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { PassportRegistry, PassportBuilderScore } from "../../../typechain-types";
import { Artifacts } from "../../shared";

chai.use(solidity);

const { expect } = chai;
const { deployContract } = waffle;

describe("PassportBuilderScore", () => {
let admin: SignerWithAddress;
let user1: SignerWithAddress;
let user2: SignerWithAddress;

let passportRegistry: PassportRegistry;
let passportBuilderScore: PassportBuilderScore;

beforeEach(async () => {
[admin, user1, user2] = await ethers.getSigners();
passportRegistry = (await deployContract(admin, Artifacts.PassportRegistry, [admin.address])) as PassportRegistry;
passportBuilderScore = (await deployContract(admin, Artifacts.PassportBuilderScore, [
passportRegistry.address,
admin.address,
])) as PassportBuilderScore;
});

describe("Deployment", () => {
it("Should set the right owner", async () => {
expect(await passportBuilderScore.owner()).to.equal(admin.address);
});

it("Should set the correct PassportRegistry address", async () => {
expect(await passportBuilderScore.passportRegistry()).to.equal(passportRegistry.address);
});
});

describe("Setting and Getting Scores", () => {
beforeEach(async () => {
await passportRegistry.setGenerationMode(true, 1); // Enable sequential mode
});

it("Should set and get the score for a passport ID", async () => {
await passportRegistry.connect(user1).create("source1");

const passportId = await passportRegistry.passportId(user1.address);
expect(passportId).to.equal(1);

await passportBuilderScore.setScore(passportId, 100);
const score = await passportBuilderScore.getScore(passportId);
expect(score).to.equal(100);
});

it("Should emit ScoreUpdated event when setting a score", async () => {
await passportRegistry.connect(user1).create("source1");

const passportId = await passportRegistry.passportId(user1.address);
expect(passportId).to.equal(1);

await expect(passportBuilderScore.setScore(passportId, 100))
.to.emit(passportBuilderScore, "ScoreUpdated")
.withArgs(passportId, 100);
});

it("Should not allow non-owner to set a score", async () => {
await passportRegistry.connect(user1).create("source1");

const passportId = await passportRegistry.passportId(user1.address);
await expect(passportBuilderScore.connect(user1).setScore(passportId, 100)).to.be.revertedWith(
`OwnableUnauthorizedAccount("${user1.address}")`
);
});

it("Should revert if setting a score for a non-existent passport ID", async () => {
await expect(passportBuilderScore.setScore(9999, 100)).to.be.revertedWith("Passport ID does not exist");
});

it("Should return 0 for a passport ID with no score set", async () => {
await passportRegistry.connect(user1).create("source1");

const passportId = await passportRegistry.passportId(user1.address);
expect(passportId).to.equal(1);

const score = await passportBuilderScore.getScore(passportId);
expect(score).to.equal(0);
});
});

describe("Changing PassportRegistry", () => {
let newPassportRegistry: PassportRegistry;

beforeEach(async () => {
newPassportRegistry = (await deployContract(admin, Artifacts.PassportRegistry, [
admin.address,
])) as PassportRegistry;
});

it("Should allow the owner to change the PassportRegistry address", async () => {
await passportBuilderScore.setPassportRegistry(newPassportRegistry.address);
expect(await passportBuilderScore.passportRegistry()).to.equal(newPassportRegistry.address);
});

it("Should emit PassportRegistryChanged event when changing the address", async () => {
await expect(passportBuilderScore.setPassportRegistry(newPassportRegistry.address))
.to.emit(passportBuilderScore, "PassportRegistryChanged")
.withArgs(passportRegistry.address, newPassportRegistry.address);
});

it("Should not allow non-owner to change the PassportRegistry address", async () => {
await expect(
passportBuilderScore.connect(user1).setPassportRegistry(newPassportRegistry.address)
).to.be.revertedWith(`OwnableUnauthorizedAccount("${user1.address}")`);
});

it("Should revert if the new address is the zero address", async () => {
await expect(passportBuilderScore.setPassportRegistry(ethers.constants.AddressZero)).to.be.revertedWith(
"Invalid address"
);
});
});
});
Loading
Loading