diff --git a/contracts/passport/StakingPassport.sol b/contracts/passport/StakingPassport.sol new file mode 100644 index 00000000..dd3ac2f3 --- /dev/null +++ b/contracts/passport/StakingPassport.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "./PassportRegistry.sol"; + +contract StakingPassport is Ownable, ReentrancyGuard { + PassportRegistry public passportRegistry; + uint256 public minimumStakeTime = 60 days; + uint256 public minimumSlashTime = 30 days; + + struct StakeData { + address staker; + uint256 passportId; + uint256 startStakeTime; + uint256 unlockStakeTime; + uint256 amount; + uint256 retrievedAmount; + uint256 slashedAmount; + string slashedReason; + } + + mapping(address => mapping(uint256 => StakeData)) public stakes; + + event Stake(address origin, uint256 passportId, uint256 amount); + event Unstake(address origin, uint256 passportId, uint256 amount); + event Slash(address staker, uint256 passportId, uint256 amount, string reason); + + constructor(address _passportRegistryAddress) Ownable(msg.sender) { + passportRegistry = PassportRegistry(_passportRegistryAddress); + } + + function stake(uint256 _passportId) payable public { + require(passportRegistry.idPassport(_passportId) != address(0), "Passport ID does not exist"); + require(msg.value > 0, "Must stake an amount higher than 0"); + + StakeData storage stakeData = stakes[msg.sender][_passportId]; + stakeData.staker = msg.sender; + stakeData.passportId = _passportId; + stakeData.startStakeTime = block.timestamp; + stakeData.unlockStakeTime = block.timestamp + minimumStakeTime; + stakeData.amount = stakeData.amount + msg.value; + + emit Stake(msg.sender, _passportId, msg.value); + } + + function unstake(uint256 _passportId, uint256 amount) public { + StakeData storage existingStake = stakes[msg.sender][_passportId]; + uint256 amountAvailableToRetrieve = existingStake.amount - existingStake.retrievedAmount - existingStake.slashedAmount; + require(amountAvailableToRetrieve > 0, "Stake must have funds to retrieve"); + require(amount <= amountAvailableToRetrieve, "You cannot withdraw more than you have staked"); + require(existingStake.unlockStakeTime < block.timestamp, "Your stake is not unlocked"); + + existingStake.retrievedAmount = existingStake.retrievedAmount + amount; + payable(msg.sender).transfer(amount); + + if(existingStake.retrievedAmount == existingStake.amount) { + delete stakes[msg.sender][_passportId]; + } + + emit Unstake(msg.sender, _passportId, amount); + } + + function slash(uint256 _passportId, uint256[] calldata amounts, address[] calldata stakers, string calldata reason) public onlyOwner { + require(amounts.length == stakers.length, "the size of the stakers must be the same as the size of amounts"); + + for (uint256 i = 0; i < amounts.length; i++) { + uint256 amount = amounts[i]; + address staker = stakers[i]; + _slash(_passportId, amount, staker, reason); + } + } + + function _slash(uint256 _passportId, uint256 amount, address staker, string calldata reason) private { + StakeData storage existingStake = stakes[staker][_passportId]; + uint256 amountAvailableToSlash = existingStake.amount - existingStake.retrievedAmount - existingStake.slashedAmount; + require(amountAvailableToSlash > 0, "There is nothing left to slash"); + require(amountAvailableToSlash >= amount, "You can't slash that much"); + require(existingStake.startStakeTime + minimumSlashTime >= block.timestamp, "Not enough time has passed to slash this stake"); + // require(reason != "", "You must name a reason to slash"); + + existingStake.slashedAmount = existingStake.slashedAmount + amount; + payable(msg.sender).transfer(amount); + + emit Slash(staker, _passportId, amount, reason); + } +} \ No newline at end of file diff --git a/scripts/passport/deployScorer.ts b/scripts/passport/deployScorer.ts index 4de7c735..29a6807c 100644 --- a/scripts/passport/deployScorer.ts +++ b/scripts/passport/deployScorer.ts @@ -1,6 +1,6 @@ import { ethers, network } from "hardhat"; -import { deployPassportBuilderScore } from "../shared"; +import { deployPassportBuilderScore, deployPassportSources, deploySmartScorer } from "../shared"; async function main() { console.log(`Deploying passport builder score at ${network.name}`); @@ -9,11 +9,26 @@ async function main() { console.log(`Admin will be ${admin.address}`); - // @TODO: Replace with registry address - const passport = await deployPassportBuilderScore("0x0", admin.address); - - console.log(`Scorer Address: ${passport.address}`); - console.log(`Scorer owner: ${await passport.owner()}`); + // base sepolia: 0xa600b3356c1440b6d6e57b0b7862dc3dfb66bc43 + // base mainnet: 0xb477A9BD2547ad61f4Ac22113172Dd909E5B2331 + const passportRegistry = "0xa600b3356c1440b6d6e57b0b7862dc3dfb66bc43"; + + const scorer = await deployPassportBuilderScore(passportRegistry, admin.address); + console.log(`Scorer Address: ${scorer.address}`); + console.log(`Scorer owner: ${await scorer.owner()}`); + + const sources = await deployPassportSources(admin.address); + console.log(`Sources Address: ${sources.address}`); + console.log(`Sources owner: ${await sources.owner()}`); + + const smartScorer = await deploySmartScorer( + admin.address, + scorer.address, + sources.address, + passportRegistry, + admin.address + ); + console.log(`Smart Scorer Address: ${smartScorer.address}`); console.log("Done"); } diff --git a/scripts/shared/index.ts b/scripts/shared/index.ts index 8452059f..0650b30c 100644 --- a/scripts/shared/index.ts +++ b/scripts/shared/index.ts @@ -6,6 +6,8 @@ import type { TalentRewardClaim, PassportBuilderScore, TalentCommunitySale, + PassportSources, + SmartBuilderScore, } from "../../typechain-types"; export async function deployPassport(owner: string): Promise { @@ -17,10 +19,10 @@ export async function deployPassport(owner: string): Promise { return deployedPassport as PassportRegistry; } -export async function deployTalentToken(): Promise { +export async function deployTalentToken(owner: string): Promise { const talentTokenContract = await ethers.getContractFactory("TalentProtocolToken"); - const deployedTalentToken = await talentTokenContract.deploy(); + const deployedTalentToken = await talentTokenContract.deploy(owner); await deployedTalentToken.deployed(); return deployedTalentToken as TalentProtocolToken; @@ -55,6 +57,36 @@ export async function deployPassportBuilderScore(registry: string, owner: string return deployedPassportBuilderScore as PassportBuilderScore; } +export async function deployPassportSources(owner: string): Promise { + const passportSources = await ethers.getContractFactory("PassportSources"); + + const deployedPassportSources = await passportSources.deploy(owner); + await deployedPassportSources.deployed(); + + return deployedPassportSources as PassportSources; +} + +export async function deploySmartScorer( + owner: string, + scorer: string, + sources: string, + registry: string, + feeCollector: string +): Promise { + const smartBuilderScoreContract = await ethers.getContractFactory("SmartBuilderScore"); + + const deployedSmartBuilderScore = await smartBuilderScoreContract.deploy( + owner, + scorer, + sources, + registry, + feeCollector + ); + await deployedSmartBuilderScore.deployed(); + + return deployedSmartBuilderScore as SmartBuilderScore; +} + export async function deployTalentCommunitySale( owner: string, tokenAddress: string, diff --git a/scripts/talent/deployTalentToken.ts b/scripts/talent/deployTalentToken.ts index b6d99938..3b4000f9 100644 --- a/scripts/talent/deployTalentToken.ts +++ b/scripts/talent/deployTalentToken.ts @@ -6,10 +6,10 @@ import { deployTalentToken } from "../shared"; const talentTokenSetup = [ { name: "SAFE Talent Token", - address: "0x0", - amount: "1000000000" - } -] + address: "0x33041027dd8F4dC82B6e825FB37ADf8f15d44053", + amount: "1000000000", + }, +]; async function main() { console.log(`Deploying Talent Token at ${network.name}`); @@ -18,7 +18,7 @@ async function main() { console.log(`Admin will be ${admin.address}`); - console.log('validating setup'); + console.log("validating setup"); for (let i = 0; i < talentTokenSetup.length; i++) { const setup = talentTokenSetup[i]; @@ -27,12 +27,15 @@ async function main() { } } - const totalAmount = talentTokenSetup.reduce((acc, setup) => acc.add(ethers.utils.parseEther(setup.amount)), BigNumber.from(0)); + const totalAmount = talentTokenSetup.reduce( + (acc, setup) => acc.add(ethers.utils.parseEther(setup.amount)), + BigNumber.from(0) + ); if (!totalAmount.eq(ethers.utils.parseEther("1000000000"))) { throw new Error(`Total amount does not match the full supply`); } - const talentToken = await deployTalentToken(); + const talentToken = await deployTalentToken(admin.address); console.log(`Talent Token Address: ${talentToken.address}`); console.log(`Talent Token owner: ${await talentToken.owner()}`);