From 30df4422f567c0fef03fdd32fe721ac79d849a42 Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Fri, 6 Dec 2024 19:29:24 +0200 Subject: [PATCH 01/14] add base implementation --- .../contracts/delegate/ProvidersDelegator.sol | 253 +++++++++ .../diamond/facets/SessionRouter.sol | 20 +- .../delegate/IProvidersDelegator.sol | 72 +++ .../contracts/mock/tokens/MorpheusToken.sol | 2 + .../test/delegate/ProviderDelegator.test.ts | 504 ++++++++++++++++++ .../test/helpers/deployers/delegate/index.ts | 1 + .../deployers/delegate/providers-delegator.ts | 25 + .../test/helpers/deployers/index.ts | 1 + 8 files changed, 874 insertions(+), 4 deletions(-) create mode 100644 smart-contracts/contracts/delegate/ProvidersDelegator.sol create mode 100644 smart-contracts/contracts/interfaces/delegate/IProvidersDelegator.sol create mode 100644 smart-contracts/test/delegate/ProviderDelegator.test.ts create mode 100644 smart-contracts/test/helpers/deployers/delegate/index.ts create mode 100644 smart-contracts/test/helpers/deployers/delegate/providers-delegator.ts diff --git a/smart-contracts/contracts/delegate/ProvidersDelegator.sol b/smart-contracts/contracts/delegate/ProvidersDelegator.sol new file mode 100644 index 00000000..b20204ce --- /dev/null +++ b/smart-contracts/contracts/delegate/ProvidersDelegator.sol @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {PRECISION} from "@solarity/solidity-lib/utils/Globals.sol"; + +import {IProvidersDelegator} from "../interfaces/delegate/IProvidersDelegator.sol"; +import {IBidStorage} from "../interfaces/storage/IBidStorage.sol"; +import {IProviderRegistry} from "../interfaces/facets/IProviderRegistry.sol"; +import {IMarketplace} from "../interfaces/facets/IMarketplace.sol"; + +contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { + using SafeERC20 for IERC20; + using Math for uint256; + + /** + * @dev The Lumerin protocol data + */ + address public lumerinDiamond; + address public token; + + /** + * @dev The fee data + */ + address public feeTreasury; + uint256 public fee; + + /** + * @dev Provider metadata + */ + string public name; + string public endpoint; + + + /** + * @dev The main contract logic data + */ + uint256 public totalStaked; + uint256 public totalRate; + uint256 public lastContractBalance; + bool public isStakeClosed; + mapping(address => Staker) public stakers; + + constructor() { + _disableInitializers(); + } + + function ProvidersDelegator_init( + address lumerinDiamond_, + address feeTreasury_, + uint256 fee_, + string memory name_, + string memory endpoint_ + ) external initializer { + __Ownable_init(); + + lumerinDiamond = lumerinDiamond_; + token = IBidStorage(lumerinDiamond).getToken(); + + setName(name_); + setEndpoint(endpoint_); + setFee(feeTreasury_, fee_); + + IERC20(token).approve(lumerinDiamond_, type(uint256).max); + } + + function setName(string memory name_) public onlyOwner { + if (bytes(name_).length == 0) { + revert InvalidNameLength(); + } + + name = name_; + + emit NameUpdated(name_); + } + + function setEndpoint(string memory endpoint_) public onlyOwner { + if (bytes(endpoint_).length == 0) { + revert InvalidEndpointLength(); + } + + endpoint = endpoint_; + + emit EndpointUpdated(endpoint_); + } + + function setFee(address feeTreasury_, uint256 fee_) public onlyOwner { + if (feeTreasury_ == address(0)) { + revert InvalidFeeTreasuryAddress(); + } + if (fee_ > PRECISION) { + revert InvalidFee(fee_, PRECISION); + } + + fee = fee_; + feeTreasury = feeTreasury_; + + emit FeeUpdated(fee_, feeTreasury_); + } + + function setIsStakeClosed(bool isStakeClosed_) public onlyOwner { + isStakeClosed = isStakeClosed_; + + emit IsStakeClosedUpdated(isStakeClosed_); + } + + function setIsRestakeDisabled(bool isRestakeDisabled_) external { + stakers[_msgSender()].isRestakeDisabled = isRestakeDisabled_; + + emit IsRestakeDisabledUpdated(_msgSender(), isRestakeDisabled_); + } + + function stake(uint256 amount_) external { + if (isStakeClosed) { + revert StakeClosed(); + } + if (amount_ == 0) { + revert InsufficientAmount(); + } + + address user_ = _msgSender(); + Staker storage staker = stakers[user_]; + + (uint256 currentRate_, uint256 contractBalance_) = getCurrentRate(); + uint256 pendingRewards_ = _getCurrentStakerRewards(currentRate_, staker); + + totalRate = currentRate_; + totalStaked += amount_; + + lastContractBalance = contractBalance_; + + staker.rate = currentRate_; + staker.staked += amount_; + staker.pendingRewards = pendingRewards_; + + IERC20(token).safeTransferFrom(user_, address(this), amount_); + IProviderRegistry(lumerinDiamond).providerRegister(address(this), amount_, endpoint); + + emit Staked(user_, staker.staked, staker.pendingRewards, staker.rate); + } + + function restake(address staker_, uint256 amount_) external { + if (_msgSender() != staker_ && _msgSender() != owner()) { + revert RestakeInvalidCaller(_msgSender(), staker_); + } + + Staker storage staker = stakers[staker_]; + if (staker.isRestakeDisabled) { + revert RestakeDisabled(staker_); + } + + (uint256 currentRate_, uint256 contractBalance_) = getCurrentRate(); + uint256 pendingRewards_ = _getCurrentStakerRewards(currentRate_, staker); + + amount_ = amount_.min(contractBalance_).min(pendingRewards_); + if (amount_ == 0) { + revert InsufficientAmount(); + } + + totalRate = currentRate_; + totalStaked += amount_; + + lastContractBalance = contractBalance_ - amount_; + + staker.rate = currentRate_; + staker.staked += amount_; + staker.pendingRewards = pendingRewards_ - amount_; + + IProviderRegistry(lumerinDiamond).providerRegister(address(this), amount_, endpoint); + + emit Restaked(staker_, staker.staked, staker.pendingRewards, staker.rate); + } + + function claim(address staker_, uint256 amount_) external { + Staker storage staker = stakers[staker_]; + + (uint256 currentRate_, uint256 contractBalance_) = getCurrentRate(); + uint256 pendingRewards_ = _getCurrentStakerRewards(currentRate_, staker); + + amount_ = amount_.min(contractBalance_).min(pendingRewards_); + if (amount_ == 0) { + revert ClaimAmountIsZero(); + } + + totalRate = currentRate_; + + lastContractBalance = contractBalance_ - amount_; + + staker.rate = currentRate_; + staker.pendingRewards = pendingRewards_ - amount_; + + uint256 feeAmount_ = (amount_ * fee) / PRECISION; + if (feeAmount_ != 0) { + IERC20(token).safeTransfer(feeTreasury, feeAmount_); + + amount_ -= feeAmount_; + + emit FeeClaimed(feeTreasury, feeAmount_); + } + + IERC20(token).safeTransfer(staker_, amount_); + + emit Claimed(staker_, staker.staked, staker.pendingRewards, staker.rate); + } + + function getCurrentRate() public view returns (uint256, uint256) { + uint256 contractBalance_ = IERC20(token).balanceOf(address(this)); + + if (totalStaked == 0) { + return (totalRate, contractBalance_); + } + + uint256 reward_ = contractBalance_ - lastContractBalance; + uint256 rate_ = totalRate + (reward_ * PRECISION) / totalStaked; + + return (rate_, contractBalance_); + } + + function getCurrentStakerRewards(address staker_) public view returns (uint256) { + Staker memory staker = stakers[staker_]; + (uint256 currentRate_,) = getCurrentRate(); + + return _getCurrentStakerRewards(currentRate_, staker); + } + + function providerDeregister() external onlyOwner { + IProviderRegistry(lumerinDiamond).providerDeregister(address(this)); + } + + function postModelBid( + bytes32 modelId_, + uint256 pricePerSecond_ + ) external onlyOwner returns (bytes32) { + return IMarketplace(lumerinDiamond).postModelBid(address(this), modelId_, pricePerSecond_); + } + + function deleteModelBid(bytes32 bidId_) external onlyOwner { + return IMarketplace(lumerinDiamond).deleteModelBid(bidId_); + } + + function _getCurrentStakerRewards(uint256 delegatorRate_, Staker memory staker_) private pure returns (uint256) { + uint256 newRewards_ = ((delegatorRate_ - staker_.rate) * staker_.staked) / PRECISION; + + return staker_.pendingRewards + newRewards_; + } + + function version() external pure returns (uint256) { + return 1; + } +} diff --git a/smart-contracts/contracts/diamond/facets/SessionRouter.sol b/smart-contracts/contracts/diamond/facets/SessionRouter.sol index e725d34c..923ef18e 100644 --- a/smart-contracts/contracts/diamond/facets/SessionRouter.sol +++ b/smart-contracts/contracts/diamond/facets/SessionRouter.sol @@ -19,6 +19,8 @@ import {LibSD} from "../../libs/LibSD.sol"; import {ISessionRouter} from "../../interfaces/facets/ISessionRouter.sol"; +import "hardhat/console.sol"; + contract SessionRouter is ISessionRouter, OwnableDiamondStorage, @@ -187,7 +189,7 @@ contract SessionRouter is } function _extractProviderApproval(bytes calldata providerApproval_) private view returns (bytes32) { - (bytes32 bidId_, uint256 chainId_,, uint128 timestamp_) = abi.decode( + (bytes32 bidId_, uint256 chainId_, , uint128 timestamp_) = abi.decode( providerApproval_, (bytes32, uint256, address, uint128) ); @@ -534,10 +536,20 @@ contract SessionRouter is bytes32 receiptHash_ = ECDSA.toEthSignedMessageHash(keccak256(receipt_)); address user_ = ECDSA.recover(receiptHash_, signature_); - if (user_ != provider_ && !isRightsDelegated(user_, provider_, DELEGATION_RULES_PROVIDER)) { - return false; + if (user_ == provider_ || isRightsDelegated(user_, provider_, DELEGATION_RULES_PROVIDER)) { + return true; + } + + if (provider_.code.length > 0) { + (bool success, bytes memory result) = provider_.staticcall(abi.encodeWithSignature("owner()")); + if (success && result.length == 32) { + address owner_ = abi.decode(result, (address)); + if (user_ == owner_) { + return true; + } + } } - return true; + return false; } } diff --git a/smart-contracts/contracts/interfaces/delegate/IProvidersDelegator.sol b/smart-contracts/contracts/interfaces/delegate/IProvidersDelegator.sol new file mode 100644 index 00000000..5843a4ec --- /dev/null +++ b/smart-contracts/contracts/interfaces/delegate/IProvidersDelegator.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IProvidersDelegator { + event NameUpdated(string name); + event EndpointUpdated(string endpoint); + event FeeUpdated(uint256 fee, address feeTreasury); + event IsStakeClosedUpdated(bool isStakeClosed); + event IsRestakeDisabledUpdated(address staker, bool isRestakeDisabled); + event Staked(address staker, uint256 staked, uint256 pendingRewards, uint256 rate); + event Restaked(address staker, uint256 staked, uint256 pendingRewards, uint256 rate); + event Claimed(address staker, uint256 staked, uint256 pendingRewards, uint256 rate); + event FeeClaimed(address feeTreasury, uint256 feeAmount); + + + + error InvalidNameLength(); + error InvalidEndpointLength(); + error InvalidFeeTreasuryAddress(); + error InvalidFee(uint256 current, uint256 max); + error StakeClosed(); + error InsufficientAmount(); + error RestakeDisabled(address staker); + error RestakeInvalidCaller(address caller, address staker); + error ClaimAmountIsZero(); + + struct Staker { + uint256 staked; + uint256 rate; + uint256 pendingRewards; + bool isRestakeDisabled; + } + + function ProvidersDelegator_init( + address lumerinDiamond_, + address feeTreasury_, + uint256 fee_, + string memory name_, + string memory endpoint_ + ) external; + + function setName(string memory name_) external; + + function setEndpoint(string memory endpoint_) external; + + function setFee(address feeTreasury_, uint256 fee_) external; + + function setIsStakeClosed(bool isStakeClosed_) external; + + function setIsRestakeDisabled(bool isRestakeDisabled_) external; + + function stake(uint256 amount_) external; + + function restake(address staker_, uint256 amount_) external; + + function claim(address staker_, uint256 amount_) external; + + function getCurrentRate() external view returns (uint256, uint256); + + function getCurrentStakerRewards(address staker_) external view returns (uint256); + + function providerDeregister() external; + + function postModelBid( + bytes32 modelId_, + uint256 pricePerSecond_ + ) external returns (bytes32); + + function deleteModelBid(bytes32 bidId_) external; + + function version() external pure returns (uint256); +} diff --git a/smart-contracts/contracts/mock/tokens/MorpheusToken.sol b/smart-contracts/contracts/mock/tokens/MorpheusToken.sol index fe5b5110..927524fc 100644 --- a/smart-contracts/contracts/mock/tokens/MorpheusToken.sol +++ b/smart-contracts/contracts/mock/tokens/MorpheusToken.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.24; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + contract MorpheusToken is ERC20 { // set the initial supply to 42 million like in whitepaper uint256 public constant INITIAL_SUPPLUY = 42_000_000 ether; diff --git a/smart-contracts/test/delegate/ProviderDelegator.test.ts b/smart-contracts/test/delegate/ProviderDelegator.test.ts new file mode 100644 index 00000000..dbdfe43d --- /dev/null +++ b/smart-contracts/test/delegate/ProviderDelegator.test.ts @@ -0,0 +1,504 @@ +import { + LumerinDiamond, + Marketplace, + ModelRegistry, + MorpheusToken, + ProviderRegistry, + ProvidersDelegator, + SessionRouter, +} from '@ethers-v6'; +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +import { payoutStart } from '../helpers/pool-helper'; + +import { DelegateRegistry } from '@/generated-types/ethers/contracts/mock/delegate-registry/src'; +import { ZERO_ADDR } from '@/scripts/utils/constants'; +import { getHex, wei } from '@/scripts/utils/utils'; +import { + deployDelegateRegistry, + deployFacetDelegation, + deployFacetMarketplace, + deployFacetModelRegistry, + deployFacetProviderRegistry, + deployFacetSessionRouter, + deployLumerinDiamond, + deployMORToken, + deployProvidersDelegator, +} from '@/test/helpers/deployers'; +import { Reverter } from '@/test/helpers/reverter'; +import { setTime } from '@/utils/block-helper'; +import { getProviderApproval, getReceipt } from '@/utils/provider-helper'; +import { DAY } from '@/utils/time'; + +describe('ProvidersDelegator', () => { + const reverter = new Reverter(); + + let OWNER: SignerWithAddress; + let DELEGATOR: SignerWithAddress; + let TREASURY: SignerWithAddress; + let KYLE: SignerWithAddress; + let SHEV: SignerWithAddress; + let ALAN: SignerWithAddress; + + let diamond: LumerinDiamond; + let providerRegistry: ProviderRegistry; + let modelRegistry: ModelRegistry; + let providersDelegator: ProvidersDelegator; + let marketplace: Marketplace; + let sessionRouter: SessionRouter; + + let token: MorpheusToken; + let delegateRegistry: DelegateRegistry; + + before(async () => { + [OWNER, DELEGATOR, TREASURY, KYLE, SHEV, ALAN] = await ethers.getSigners(); + + [diamond, token, delegateRegistry] = await Promise.all([ + deployLumerinDiamond(), + deployMORToken(), + deployDelegateRegistry(), + ]); + + [providerRegistry, modelRegistry, sessionRouter, marketplace] = await Promise.all([ + deployFacetProviderRegistry(diamond), + deployFacetModelRegistry(diamond), + deployFacetSessionRouter(diamond, OWNER), + deployFacetMarketplace(diamond, token, wei(0.0001), wei(900)), + deployFacetDelegation(diamond, delegateRegistry), + ]); + + providersDelegator = await deployProvidersDelegator( + diamond, + await TREASURY.getAddress(), + wei(0.2, 25), + 'DLNAME', + 'ENDPOINT', + ); + + await token.transfer(KYLE, wei(1000)); + await token.transfer(SHEV, wei(1000)); + await token.transfer(DELEGATOR, wei(1000)); + await token.transfer(ALAN, wei(1000)); + + await token.connect(OWNER).approve(sessionRouter, wei(1000)); + await token.connect(ALAN).approve(sessionRouter, wei(1000)); + await token.connect(KYLE).approve(providersDelegator, wei(1000)); + await token.connect(SHEV).approve(providersDelegator, wei(1000)); + await token.connect(DELEGATOR).approve(modelRegistry, wei(1000)); + + await reverter.snapshot(); + }); + + afterEach(reverter.revert); + + describe('#ProvidersDelegator_init', () => { + it('should revert if try to call init function twice', async () => { + await expect( + providersDelegator.ProvidersDelegator_init(OWNER, await TREASURY.getAddress(), 1, '', ''), + ).to.be.rejectedWith('Initializable: contract is already initialized'); + }); + }); + + describe('#setName', () => { + it('should set the provider name', async () => { + await providersDelegator.setName('TEST'); + + expect(await providersDelegator.name()).eq('TEST'); + }); + it('should throw error when name is zero', async () => { + await expect(providersDelegator.setName('')).to.be.revertedWithCustomError( + providersDelegator, + 'InvalidNameLength', + ); + }); + it('should throw error when caller is not an owner', async () => { + await expect(providersDelegator.connect(KYLE).setName('')).to.be.revertedWith('Ownable: caller is not the owner'); + }); + }); + + describe('#setEndpoint', () => { + it('should set the provider endpoint', async () => { + await providersDelegator.setEndpoint('TEST'); + + expect(await providersDelegator.endpoint()).eq('TEST'); + }); + it('should throw error when endpoint is zero', async () => { + await expect(providersDelegator.setEndpoint('')).to.be.revertedWithCustomError( + providersDelegator, + 'InvalidEndpointLength', + ); + }); + it('should throw error when caller is not an owner', async () => { + await expect(providersDelegator.connect(KYLE).setEndpoint('')).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('#setFee', () => { + it('should set the provider fee', async () => { + await providersDelegator.setFee(KYLE, wei(0.1, 25)); + + expect(await providersDelegator.fee()).eq(wei(0.1, 25)); + expect(await providersDelegator.feeTreasury()).eq(KYLE); + }); + it('should throw error when fee treasury is invalid', async () => { + await expect(providersDelegator.setFee(ZERO_ADDR, wei(0.1, 25))).to.be.revertedWithCustomError( + providersDelegator, + 'InvalidFeeTreasuryAddress', + ); + }); + it('should throw error when fee is invalid', async () => { + await expect(providersDelegator.setFee(KYLE, wei(1.01, 25))).to.be.revertedWithCustomError( + providersDelegator, + 'InvalidFee', + ); + }); + it('should throw error when caller is not an owner', async () => { + await expect(providersDelegator.connect(KYLE).setFee(KYLE, wei(1.01, 25))).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('#setIsStakeClosed', () => { + it('should set the isStakeClosed flag', async () => { + await providersDelegator.setIsStakeClosed(true); + + expect(await providersDelegator.isStakeClosed()).eq(true); + }); + it('should throw error when caller is not an owner', async () => { + await expect(providersDelegator.connect(KYLE).setIsStakeClosed(true)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('#stake', () => { + it('should stake tokens, one staker', async () => { + await providersDelegator.connect(KYLE).stake(wei(100)); + + const staker = await providersDelegator.stakers(KYLE); + expect(staker.staked).to.eq(wei(100)); + expect(staker.pendingRewards).to.eq(wei(0)); + expect(staker.isRestakeDisabled).to.eq(false); + expect(await providersDelegator.totalStaked()).to.eq(wei(100)); + + expect(await token.balanceOf(providersDelegator)).to.eq(wei(0)); + expect(await token.balanceOf(providerRegistry)).to.eq(wei(100)); + expect(await token.balanceOf(KYLE)).to.eq(wei(900)); + }); + it('should stake tokens, two staker', async () => { + await providersDelegator.connect(KYLE).stake(wei(100)); + + const staker1 = await providersDelegator.stakers(KYLE); + expect(staker1.staked).to.eq(wei(100)); + expect(staker1.pendingRewards).to.eq(wei(0)); + expect(staker1.isRestakeDisabled).to.eq(false); + expect(await providersDelegator.totalStaked()).to.eq(wei(100)); + + await providersDelegator.connect(SHEV).stake(wei(200)); + + const staker2 = await providersDelegator.stakers(SHEV); + expect(staker2.staked).to.eq(wei(200)); + expect(staker2.pendingRewards).to.eq(wei(0)); + expect(staker2.isRestakeDisabled).to.eq(false); + expect(await providersDelegator.totalStaked()).to.eq(wei(300)); + + expect(await token.balanceOf(providersDelegator)).to.eq(wei(0)); + expect(await token.balanceOf(providerRegistry)).to.eq(wei(300)); + expect(await token.balanceOf(KYLE)).to.eq(wei(900)); + expect(await token.balanceOf(SHEV)).to.eq(wei(800)); + }); + it('should throw error when the stake is too low', async () => { + await expect(providersDelegator.connect(KYLE).stake(wei(0))).to.be.revertedWithCustomError( + providersDelegator, + 'InsufficientAmount', + ); + }); + it('should throw error when the stake closed', async () => { + providersDelegator.setIsStakeClosed(true); + await expect(providersDelegator.connect(KYLE).stake(wei(1))).to.be.revertedWithCustomError( + providersDelegator, + 'StakeClosed', + ); + }); + }); + + describe('#claim', () => { + beforeEach(async () => { + await providersDelegator.setFee(TREASURY, wei(0.2, 25)); + }); + + it('should correctly claim, one staker, full claim', async () => { + await providersDelegator.connect(KYLE).stake(wei(100)); + + await token.transfer(providersDelegator, wei(10)); + + await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.eq(wei(908)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(2)); + }); + it('should correctly claim, one staker, partial claim', async () => { + await providersDelegator.connect(KYLE).stake(wei(100)); + + await token.transfer(providersDelegator, wei(20)); + + await providersDelegator.connect(KYLE).claim(KYLE, wei(5)); + expect(await token.balanceOf(KYLE)).to.eq(wei(904)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(1)); + + await providersDelegator.connect(KYLE).claim(KYLE, wei(10)); + expect(await token.balanceOf(KYLE)).to.eq(wei(912)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(3)); + + await providersDelegator.connect(KYLE).claim(KYLE, wei(5)); + expect(await token.balanceOf(KYLE)).to.eq(wei(916)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(4)); + }); + it('should correctly claim, two stakers, full claim, enter when no rewards distributed', async () => { + await providersDelegator.connect(KYLE).stake(wei(100)); + await providersDelegator.connect(SHEV).stake(wei(300)); + + await token.transfer(providersDelegator, wei(40)); + + await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.eq(wei(908)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(2)); + + await providersDelegator.connect(SHEV).claim(SHEV, wei(9999)); + expect(await token.balanceOf(SHEV)).to.eq(wei(724)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(2 + 6)); + }); + it('should correctly claim, two stakers, partial claim, enter when rewards distributed', async () => { + await providersDelegator.connect(KYLE).stake(wei(100)); + + await token.transfer(providersDelegator, wei(10)); + + await providersDelegator.connect(SHEV).stake(wei(300)); + + await token.transfer(providersDelegator, wei(40)); + + await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.eq(wei(916)); // 10 + 25% from 40 + expect(await token.balanceOf(TREASURY)).to.eq(wei(4)); + + await providersDelegator.connect(SHEV).claim(SHEV, wei(20)); + expect(await token.balanceOf(SHEV)).to.eq(wei(716)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(4 + 4)); + + await token.transfer(providersDelegator, wei(100)); + + await providersDelegator.connect(SHEV).claim(SHEV, wei(20)); + expect(await token.balanceOf(SHEV)).to.eq(wei(732)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(4 + 4 + 4)); + + await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.eq(wei(936)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(4 + 4 + 4 + 5)); + + await providersDelegator.connect(SHEV).claim(SHEV, wei(999)); + expect(await token.balanceOf(SHEV)).to.eq(wei(784)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(4 + 4 + 4 + 5 + 13)); + }); + it('should correctly claim, full amount without fee', async () => { + await providersDelegator.setFee(TREASURY, wei(0, 25)); + + await providersDelegator.connect(KYLE).stake(wei(100)); + expect(await providersDelegator.getCurrentStakerRewards(KYLE)).to.eq(wei(0)); + + await token.transfer(providersDelegator, wei(10)); + expect(await providersDelegator.getCurrentStakerRewards(KYLE)).to.eq(wei(10)); + + await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.eq(wei(910)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); + }); + it('should throw error when nothing to claim', async () => { + await expect(providersDelegator.connect(KYLE).claim(KYLE, wei(999))).to.be.revertedWithCustomError( + providersDelegator, + 'ClaimAmountIsZero', + ); + }); + }); + + describe('#restake', () => { + beforeEach(async () => { + await providersDelegator.setFee(TREASURY, wei(0.2, 25)); + }); + + it('should correctly restake, two stakers, full restake', async () => { + await providersDelegator.connect(KYLE).stake(wei(100)); + await providersDelegator.connect(SHEV).stake(wei(300)); + + await token.transfer(providersDelegator, wei(100)); + + await providersDelegator.connect(OWNER).restake(KYLE, wei(9999)); + expect((await providersDelegator.stakers(KYLE)).staked).to.eq(wei(125)); + expect(await token.balanceOf(KYLE)).to.eq(wei(900)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); + + await token.transfer(providersDelegator, wei(100)); + + await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.closeTo(wei(900 + 29.41 * 0.8), wei(0.01)); + expect(await token.balanceOf(TREASURY)).to.closeTo(wei(29.41 * 0.2), wei(0.01)); + + await providersDelegator.connect(SHEV).claim(SHEV, wei(9999)); + expect(await token.balanceOf(SHEV)).to.closeTo(wei(700 + 75 * 0.8 + 70.58 * 0.8), wei(0.01)); + expect(await token.balanceOf(TREASURY)).to.closeTo(wei(29.41 * 0.2 + 75 * 0.2 + 70.58 * 0.2), wei(0.01)); + }); + + it('should correctly restake, two stakers, partial restake', async () => { + await providersDelegator.connect(KYLE).stake(wei(100)); + await providersDelegator.connect(SHEV).stake(wei(300)); + + await token.transfer(providersDelegator, wei(100)); + + await providersDelegator.connect(OWNER).restake(KYLE, wei(20)); + expect((await providersDelegator.stakers(KYLE)).staked).to.eq(wei(120)); + expect(await token.balanceOf(KYLE)).to.eq(wei(900)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); + + await token.transfer(providersDelegator, wei(100)); + + await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.closeTo(wei(900 + 5 * 0.8 + 28.57 * 0.8), wei(0.01)); + expect(await token.balanceOf(TREASURY)).to.closeTo(wei(5 * 0.2 + 28.57 * 0.2), wei(0.01)); + + await providersDelegator.connect(SHEV).claim(SHEV, wei(9999)); + expect(await token.balanceOf(SHEV)).to.closeTo(wei(700 + 75 * 0.8 + 71.42 * 0.8), wei(0.01)); + expect(await token.balanceOf(TREASURY)).to.closeTo( + wei(5 * 0.2 + 28.57 * 0.2 + 75 * 0.2 + 71.42 * 0.2), + wei(0.01), + ); + }); + it('should throw error when restake caller is invalid', async () => { + await expect(providersDelegator.connect(KYLE).restake(SHEV, wei(999))).to.be.revertedWithCustomError( + providersDelegator, + 'RestakeInvalidCaller', + ); + }); + it('should throw error when restake caller is invalid', async () => { + await providersDelegator.connect(SHEV).setIsRestakeDisabled(true); + await expect(providersDelegator.restake(SHEV, wei(999))).to.be.revertedWithCustomError( + providersDelegator, + 'RestakeDisabled', + ); + }); + it('should throw error when restake amount is zero', async () => { + await expect(providersDelegator.restake(SHEV, wei(0))).to.be.revertedWithCustomError( + providersDelegator, + 'InsufficientAmount', + ); + }); + }); + + describe('#providerDeregister', () => { + beforeEach(async () => { + await providersDelegator.setFee(TREASURY, wei(0.2, 25)); + }); + + it('should deregister the provider', async () => { + await providersDelegator.connect(KYLE).stake(wei(100)); + await providersDelegator.providerDeregister(); + + await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.eq(wei(980)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(20)); + }); + it('should throw error when caller is not an owner', async () => { + await expect(providersDelegator.connect(KYLE).providerDeregister()).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('#postModelBid, #deleteModelBid', () => { + const baseModelId = getHex(Buffer.from('1')); + + it('should deregister the model bid and delete it', async () => { + // Register provider + await providersDelegator.connect(SHEV).stake(wei(300)); + + // Register model + await modelRegistry + .connect(DELEGATOR) + .modelRegister(DELEGATOR, baseModelId, getHex(Buffer.from('ipfs://ipfsaddress')), 0, wei(100), 'name', [ + 'tag_1', + ]); + const modelId = await modelRegistry.getModelId(DELEGATOR, baseModelId); + + // Register bid + await providersDelegator.postModelBid(modelId, wei(0.0001)); + const bidId = await marketplace.getBidId(await providersDelegator.getAddress(), modelId, 0); + + await providersDelegator.deleteModelBid(bidId); + }); + it('should throw error when caller is not an owner', async () => { + await expect(providersDelegator.connect(KYLE).postModelBid(baseModelId, wei(0.0001))).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + it('should throw error when caller is not an owner', async () => { + await expect(providersDelegator.connect(KYLE).deleteModelBid(baseModelId)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('#version', () => { + it('should return the correct contract version', async () => { + expect(await providersDelegator.version()).to.eq(1); + }); + }); + + describe('full flow', () => { + const baseModelId = getHex(Buffer.from('1')); + + it('should deregister the provider', async () => { + // Register provider + await providersDelegator.setFee(TREASURY, wei(0.2, 25)); + await providersDelegator.connect(KYLE).stake(wei(100)); + await providersDelegator.connect(SHEV).stake(wei(300)); + + // Register model + await modelRegistry + .connect(DELEGATOR) + .modelRegister(DELEGATOR, baseModelId, getHex(Buffer.from('ipfs://ipfsaddress')), 0, wei(100), 'name', [ + 'tag_1', + ]); + const modelId = await modelRegistry.getModelId(DELEGATOR, baseModelId); + + // Register bid + await providersDelegator.postModelBid(modelId, wei(0.0001)); + const bidId = await marketplace.getBidId(await providersDelegator.getAddress(), modelId, 0); + + await setTime(payoutStart + 10 * DAY); + const { msg, signature } = await getProviderApproval(OWNER, ALAN, bidId); + await sessionRouter.connect(ALAN).openSession(ALAN, wei(50), false, msg, signature); + const sessionId = await sessionRouter.getSessionId(ALAN, providersDelegator, bidId, 0); + + const sessionTreasuryBalanceBefore = await token.balanceOf(OWNER); + + await setTime(payoutStart + 15 * DAY); + const { msg: receiptMsg } = await getReceipt(OWNER, sessionId, 0, 0); + const { signature: receiptSig } = await getReceipt(OWNER, sessionId, 0, 0); + await sessionRouter.connect(ALAN).closeSession(receiptMsg, receiptSig); + + const sessionTreasuryBalanceAfter = await token.balanceOf(OWNER); + const reward = sessionTreasuryBalanceBefore - sessionTreasuryBalanceAfter; + + await providersDelegator.claim(KYLE, wei(9999)); + await providersDelegator.claim(SHEV, wei(9999)); + expect(await token.balanceOf(KYLE)).to.eq(wei(900) + BigInt(Number(reward.toString()) * 0.25 * 0.8)); + expect(await token.balanceOf(SHEV)).to.eq(wei(700) + BigInt(Number(reward.toString()) * 0.75 * 0.8)); + expect(await token.balanceOf(TREASURY)).to.eq(BigInt(Number(reward.toString()) * 0.2)); + }); + }); +}); + +// npm run generate-types && npx hardhat test "test/delegate/ProviderDelegator.test.ts" +// npx hardhat coverage --solcoverjs ./.solcover.ts --testfiles "test/delegate/ProviderDelegator.test.ts" diff --git a/smart-contracts/test/helpers/deployers/delegate/index.ts b/smart-contracts/test/helpers/deployers/delegate/index.ts new file mode 100644 index 00000000..25be1179 --- /dev/null +++ b/smart-contracts/test/helpers/deployers/delegate/index.ts @@ -0,0 +1 @@ +export * from './providers-delegator'; diff --git a/smart-contracts/test/helpers/deployers/delegate/providers-delegator.ts b/smart-contracts/test/helpers/deployers/delegate/providers-delegator.ts new file mode 100644 index 00000000..d2d6ce60 --- /dev/null +++ b/smart-contracts/test/helpers/deployers/delegate/providers-delegator.ts @@ -0,0 +1,25 @@ +import { BigNumberish } from 'ethers'; +import { ethers } from 'hardhat'; + +import { LumerinDiamond, ProvidersDelegator } from '@/generated-types/ethers'; + +export const deployProvidersDelegator = async ( + diamond: LumerinDiamond, + feeTreasury: string, + fee: BigNumberish, + name: string, + endpoint: string, +): Promise => { + const [implFactory, proxyFactory] = await Promise.all([ + ethers.getContractFactory('ProvidersDelegator'), + ethers.getContractFactory('ERC1967Proxy'), + ]); + + const impl = await implFactory.deploy(); + const proxy = await proxyFactory.deploy(impl, '0x'); + const contract = implFactory.attach(proxy) as ProvidersDelegator; + + await contract.ProvidersDelegator_init(diamond, feeTreasury, fee, name, endpoint); + + return contract; +}; diff --git a/smart-contracts/test/helpers/deployers/index.ts b/smart-contracts/test/helpers/deployers/index.ts index 6d1b8c98..c9eb775c 100644 --- a/smart-contracts/test/helpers/deployers/index.ts +++ b/smart-contracts/test/helpers/deployers/index.ts @@ -1,2 +1,3 @@ +export * from './delegate'; export * from './diamond'; export * from './mock'; From 16df1e1ad04ed0e90a57f9aeafd896ff177446ce Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Tue, 10 Dec 2024 16:34:17 +0200 Subject: [PATCH 02/14] add the factory --- .../contracts/delegate/DelegatorFactory.sol | 88 ++++++++ .../contracts/interfaces/utils/IOwnable.sol | 15 ++ smart-contracts/contracts/mock/UUPSMock.sol | 12 ++ .../test/delegate/DelegatorFactory.test.ts | 200 ++++++++++++++++++ .../test/delegate/ProviderDelegator.test.ts | 2 +- .../deployers/delegate/delegator-factory.ts | 20 ++ .../test/helpers/deployers/delegate/index.ts | 1 + 7 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 smart-contracts/contracts/delegate/DelegatorFactory.sol create mode 100644 smart-contracts/contracts/interfaces/utils/IOwnable.sol create mode 100644 smart-contracts/contracts/mock/UUPSMock.sol create mode 100644 smart-contracts/test/delegate/DelegatorFactory.test.ts create mode 100644 smart-contracts/test/helpers/deployers/delegate/delegator-factory.ts diff --git a/smart-contracts/contracts/delegate/DelegatorFactory.sol b/smart-contracts/contracts/delegate/DelegatorFactory.sol new file mode 100644 index 00000000..4f149bab --- /dev/null +++ b/smart-contracts/contracts/delegate/DelegatorFactory.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; + +import {IProvidersDelegator} from "../interfaces/delegate/IProvidersDelegator.sol"; +import {IOwnable} from "../interfaces/utils/IOwnable.sol"; + +contract DelegatorFactory is OwnableUpgradeable, PausableUpgradeable, UUPSUpgradeable { + address public lumerinDiamond; + address public beacon; + mapping(address => address[]) public proxies; + + event ProxyDeployed(address indexed proxyAddress); + event ImplementationUpdated(address indexed newImplementation); + + constructor() { + _disableInitializers(); + } + + function DelegatorFactory_init(address _lumerinDiamond, address _implementation) external initializer { + __Pausable_init(); + __Ownable_init(); + __UUPSUpgradeable_init(); + + lumerinDiamond = _lumerinDiamond; + + beacon = address(new UpgradeableBeacon(_implementation)); + } + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } + + function deployProxy( + address feeTreasury_, + uint256 fee_, + string memory name_, + string memory endpoint_ + ) external whenNotPaused returns (address) { + bytes32 salt_ = _calculatePoolSalt(_msgSender()); + address proxy_ = address(new BeaconProxy{salt: salt_}(beacon, bytes(""))); + + proxies[_msgSender()].push(address(proxy_)); + + IProvidersDelegator(proxy_).ProvidersDelegator_init(lumerinDiamond, feeTreasury_, fee_, name_, endpoint_); + IOwnable(proxy_).transferOwnership(_msgSender()); + + emit ProxyDeployed(address(proxy_)); + + return address(proxy_); + } + + function predictProxyAddress(address _deployer) external view returns (address) { + bytes32 salt_ = _calculatePoolSalt(_deployer); + + bytes32 bytecodeHash_ = keccak256( + abi.encodePacked(type(BeaconProxy).creationCode, abi.encode(address(beacon), bytes(""))) + ); + + return Create2.computeAddress(salt_, bytecodeHash_); + } + + function updateImplementation(address _newImplementation) external onlyOwner { + UpgradeableBeacon(beacon).upgradeTo(_newImplementation); + + emit ImplementationUpdated(_newImplementation); + } + + function version() external pure returns (uint256) { + return 1; + } + + function _calculatePoolSalt(address sender_) internal view returns (bytes32) { + return keccak256(abi.encodePacked(sender_, proxies[sender_].length)); + } + + function _authorizeUpgrade(address) internal view override onlyOwner {} +} diff --git a/smart-contracts/contracts/interfaces/utils/IOwnable.sol b/smart-contracts/contracts/interfaces/utils/IOwnable.sol new file mode 100644 index 00000000..0d1aabc9 --- /dev/null +++ b/smart-contracts/contracts/interfaces/utils/IOwnable.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IOwnable { + /** + * @dev Returns the address of the current owner. + */ + function owner() external view returns (address); + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner_) external; +} diff --git a/smart-contracts/contracts/mock/UUPSMock.sol b/smart-contracts/contracts/mock/UUPSMock.sol new file mode 100644 index 00000000..585fa08b --- /dev/null +++ b/smart-contracts/contracts/mock/UUPSMock.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +contract UUPSMock is UUPSUpgradeable { + function version() external pure returns (uint256) { + return 999; + } + + function _authorizeUpgrade(address) internal view override {} +} diff --git a/smart-contracts/test/delegate/DelegatorFactory.test.ts b/smart-contracts/test/delegate/DelegatorFactory.test.ts new file mode 100644 index 00000000..92728733 --- /dev/null +++ b/smart-contracts/test/delegate/DelegatorFactory.test.ts @@ -0,0 +1,200 @@ +import { + DelegatorFactory, + LumerinDiamond, + MorpheusToken, + ProvidersDelegator, + ProvidersDelegator__factory, + UUPSMock, +} from '@ethers-v6'; +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +import { wei } from '@/scripts/utils/utils'; +import { + deployDelegatorFactory, + deployFacetMarketplace, + deployFacetProviderRegistry, + deployLumerinDiamond, + deployMORToken, +} from '@/test/helpers/deployers'; +import { Reverter } from '@/test/helpers/reverter'; + +describe('DelegatorFactory', () => { + const reverter = new Reverter(); + + let OWNER: SignerWithAddress; + let KYLE: SignerWithAddress; + let SHEV: SignerWithAddress; + + let diamond: LumerinDiamond; + let delegatorFactory: DelegatorFactory; + + let token: MorpheusToken; + + before(async () => { + [OWNER, KYLE, SHEV] = await ethers.getSigners(); + + [diamond, token] = await Promise.all([deployLumerinDiamond(), deployMORToken()]); + await Promise.all([ + deployFacetProviderRegistry(diamond), + deployFacetMarketplace(diamond, token, wei(0.0001), wei(900)), + ]); + + delegatorFactory = await deployDelegatorFactory(diamond); + + await reverter.snapshot(); + }); + + afterEach(reverter.revert); + + describe('UUPS', () => { + describe('#DelegatorFactory_init', () => { + it('should revert if try to call init function twice', async () => { + await expect(delegatorFactory.DelegatorFactory_init(OWNER, OWNER)).to.be.rejectedWith( + 'Initializable: contract is already initialized', + ); + }); + }); + describe('#version', () => { + it('should return correct version', async () => { + expect(await delegatorFactory.version()).to.eq(1); + }); + }); + describe('#upgradeTo', () => { + it('should upgrade to the new version', async () => { + const factory = await ethers.getContractFactory('UUPSMock'); + const newImpl = await factory.deploy(); + + await delegatorFactory.upgradeTo(newImpl); + const newDelegatorFactory = newImpl.attach(delegatorFactory) as UUPSMock; + + expect(await newDelegatorFactory.version()).to.eq(999); + }); + it('should throw error when caller is not an owner', async () => { + await expect(delegatorFactory.connect(KYLE).upgradeTo(KYLE)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + }); + + describe('#deployProxy', () => { + let providersDelegatorFactory: ProvidersDelegator__factory; + + before(async () => { + providersDelegatorFactory = await ethers.getContractFactory('ProvidersDelegator'); + }); + + it('should deploy a new proxy', async () => { + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + + const proxy = providersDelegatorFactory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegator; + + expect(await proxy.owner()).to.eq(SHEV); + expect(await proxy.fee()).to.eq(wei(0.1, 25)); + expect(await proxy.feeTreasury()).to.eq(KYLE); + expect(await proxy.name()).to.eq('name'); + expect(await proxy.endpoint()).to.eq('endpoint'); + }); + it('should deploy new proxies', async () => { + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1'); + await delegatorFactory.connect(SHEV).deployProxy(SHEV, wei(0.2, 25), 'name2', 'endpoint2'); + await delegatorFactory.connect(KYLE).deployProxy(SHEV, wei(0.3, 25), 'name3', 'endpoint3'); + + let proxy = providersDelegatorFactory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegator; + expect(await proxy.owner()).to.eq(SHEV); + expect(await proxy.fee()).to.eq(wei(0.2, 25)); + expect(await proxy.feeTreasury()).to.eq(SHEV); + expect(await proxy.name()).to.eq('name2'); + expect(await proxy.endpoint()).to.eq('endpoint2'); + + proxy = providersDelegatorFactory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegator; + expect(await proxy.owner()).to.eq(KYLE); + expect(await proxy.fee()).to.eq(wei(0.3, 25)); + expect(await proxy.feeTreasury()).to.eq(SHEV); + expect(await proxy.name()).to.eq('name3'); + expect(await proxy.endpoint()).to.eq('endpoint3'); + }); + describe('#pause, #unpause', () => { + it('should revert when paused and not after the unpause', async () => { + await delegatorFactory.pause(); + await expect( + delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1'), + ).to.be.rejectedWith('Pausable: paused'); + + await delegatorFactory.unpause(); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1'); + }); + it('should throw error when caller is not an owner', async () => { + await expect(delegatorFactory.connect(KYLE).pause()).to.be.revertedWith('Ownable: caller is not the owner'); + }); + it('should throw error when caller is not an owner', async () => { + await expect(delegatorFactory.connect(KYLE).unpause()).to.be.revertedWith('Ownable: caller is not the owner'); + }); + }); + }); + + describe('#predictProxyAddress', () => { + it('should predict a proxy address', async () => { + const predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + + const proxyAddress = await delegatorFactory.proxies(SHEV, 0); + + expect(proxyAddress).to.eq(predictedProxyAddress); + }); + it('should predict proxy addresses', async () => { + let predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + expect(await delegatorFactory.proxies(SHEV, 0)).to.eq(predictedProxyAddress); + + predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + expect(await delegatorFactory.proxies(SHEV, 1)).to.eq(predictedProxyAddress); + + predictedProxyAddress = await delegatorFactory.predictProxyAddress(KYLE); + await delegatorFactory.connect(KYLE).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + expect(await delegatorFactory.proxies(KYLE, 0)).to.eq(predictedProxyAddress); + + predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + expect(await delegatorFactory.proxies(SHEV, 2)).to.eq(predictedProxyAddress); + }); + }); + + describe('#updateImplementation', () => { + it('should update proxies implementation', async () => { + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + await delegatorFactory.connect(KYLE).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + + const factory = await ethers.getContractFactory('UUPSMock'); + const newImpl = await factory.deploy(); + + let proxy = factory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegator; + expect(await proxy.version()).to.eq(1); + proxy = factory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegator; + expect(await proxy.version()).to.eq(1); + proxy = factory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegator; + expect(await proxy.version()).to.eq(1); + + await delegatorFactory.updateImplementation(newImpl); + + proxy = factory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegator; + expect(await proxy.version()).to.eq(999); + proxy = factory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegator; + expect(await proxy.version()).to.eq(999); + proxy = factory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegator; + expect(await proxy.version()).to.eq(999); + }); + it('should throw error when caller is not an owner', async () => { + await expect(delegatorFactory.connect(KYLE).updateImplementation(KYLE)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); +}); + +// npm run generate-types && npx hardhat test "test/delegate/DelegatorFactory.test" +// npx hardhat coverage --solcoverjs ./.solcover.ts --testfiles "test/delegate/DelegatorFactory.test" diff --git a/smart-contracts/test/delegate/ProviderDelegator.test.ts b/smart-contracts/test/delegate/ProviderDelegator.test.ts index dbdfe43d..2a191b63 100644 --- a/smart-contracts/test/delegate/ProviderDelegator.test.ts +++ b/smart-contracts/test/delegate/ProviderDelegator.test.ts @@ -219,7 +219,7 @@ describe('ProvidersDelegator', () => { ); }); it('should throw error when the stake closed', async () => { - providersDelegator.setIsStakeClosed(true); + await providersDelegator.setIsStakeClosed(true); await expect(providersDelegator.connect(KYLE).stake(wei(1))).to.be.revertedWithCustomError( providersDelegator, 'StakeClosed', diff --git a/smart-contracts/test/helpers/deployers/delegate/delegator-factory.ts b/smart-contracts/test/helpers/deployers/delegate/delegator-factory.ts new file mode 100644 index 00000000..dc4c8a6e --- /dev/null +++ b/smart-contracts/test/helpers/deployers/delegate/delegator-factory.ts @@ -0,0 +1,20 @@ +import { ethers } from 'hardhat'; + +import { DelegatorFactory, LumerinDiamond } from '@/generated-types/ethers'; + +export const deployDelegatorFactory = async (diamond: LumerinDiamond): Promise => { + const [providersDelegatorImplFactory, delegatorFactoryImplFactory, proxyFactory] = await Promise.all([ + ethers.getContractFactory('ProvidersDelegator'), + ethers.getContractFactory('DelegatorFactory'), + ethers.getContractFactory('ERC1967Proxy'), + ]); + + const delegatorFactoryImpl = await delegatorFactoryImplFactory.deploy(); + const proxy = await proxyFactory.deploy(delegatorFactoryImpl, '0x'); + const delegatorFactory = delegatorFactoryImpl.attach(proxy) as DelegatorFactory; + + const providersDelegatorImpl = await providersDelegatorImplFactory.deploy(); + await delegatorFactory.DelegatorFactory_init(diamond, providersDelegatorImpl); + + return delegatorFactory; +}; diff --git a/smart-contracts/test/helpers/deployers/delegate/index.ts b/smart-contracts/test/helpers/deployers/delegate/index.ts index 25be1179..462ee559 100644 --- a/smart-contracts/test/helpers/deployers/delegate/index.ts +++ b/smart-contracts/test/helpers/deployers/delegate/index.ts @@ -1 +1,2 @@ +export * from './delegator-factory'; export * from './providers-delegator'; From 149310a9a3c4c4cae7d22890c4d0dc0d8b0ca26e Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Tue, 10 Dec 2024 16:48:06 +0200 Subject: [PATCH 03/14] review fixes #1 --- smart-contracts/contracts/delegate/ProvidersDelegator.sol | 5 +++-- smart-contracts/contracts/diamond/facets/SessionRouter.sol | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/smart-contracts/contracts/delegate/ProvidersDelegator.sol b/smart-contracts/contracts/delegate/ProvidersDelegator.sol index b20204ce..6f09a9e7 100644 --- a/smart-contracts/contracts/delegate/ProvidersDelegator.sol +++ b/smart-contracts/contracts/delegate/ProvidersDelegator.sol @@ -120,13 +120,15 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { if (amount_ == 0) { revert InsufficientAmount(); } - + address user_ = _msgSender(); Staker storage staker = stakers[user_]; (uint256 currentRate_, uint256 contractBalance_) = getCurrentRate(); uint256 pendingRewards_ = _getCurrentStakerRewards(currentRate_, staker); + IERC20(token).safeTransferFrom(user_, address(this), amount_); + totalRate = currentRate_; totalStaked += amount_; @@ -136,7 +138,6 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { staker.staked += amount_; staker.pendingRewards = pendingRewards_; - IERC20(token).safeTransferFrom(user_, address(this), amount_); IProviderRegistry(lumerinDiamond).providerRegister(address(this), amount_, endpoint); emit Staked(user_, staker.staked, staker.pendingRewards, staker.rate); diff --git a/smart-contracts/contracts/diamond/facets/SessionRouter.sol b/smart-contracts/contracts/diamond/facets/SessionRouter.sol index 923ef18e..53e18b79 100644 --- a/smart-contracts/contracts/diamond/facets/SessionRouter.sol +++ b/smart-contracts/contracts/diamond/facets/SessionRouter.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.24; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -19,8 +21,6 @@ import {LibSD} from "../../libs/LibSD.sol"; import {ISessionRouter} from "../../interfaces/facets/ISessionRouter.sol"; -import "hardhat/console.sol"; - contract SessionRouter is ISessionRouter, OwnableDiamondStorage, @@ -34,6 +34,7 @@ contract SessionRouter is using LibSD for LibSD.SD; using SafeERC20 for IERC20; using EnumerableSet for EnumerableSet.Bytes32Set; + using Address for address; function __SessionRouter_init( address fundingAccount_, @@ -540,7 +541,7 @@ contract SessionRouter is return true; } - if (provider_.code.length > 0) { + if (provider_.isContract()) { (bool success, bytes memory result) = provider_.staticcall(abi.encodeWithSignature("owner()")); if (success && result.length == 32) { address owner_ = abi.decode(result, (address)); From 8c69bfabe53e085ea17d6c27f586e92ee5411352 Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Fri, 13 Dec 2024 13:31:41 +0200 Subject: [PATCH 04/14] review fixes, add the deregister process --- .../contracts/delegate/DelegatorFactory.sol | 42 +++--- .../contracts/delegate/ProvidersDelegator.sol | 85 +++++++----- .../interfaces/delegate/IDelegatorFactory.sol | 59 +++++++++ .../delegate/IProvidersDelegator.sol | 100 ++++++++++++-- .../test/delegate/DelegatorFactory.test.ts | 28 ++-- .../test/delegate/ProviderDelegator.test.ts | 124 ++++++++++++------ .../deployers/delegate/providers-delegator.ts | 12 +- 7 files changed, 327 insertions(+), 123 deletions(-) create mode 100644 smart-contracts/contracts/interfaces/delegate/IDelegatorFactory.sol diff --git a/smart-contracts/contracts/delegate/DelegatorFactory.sol b/smart-contracts/contracts/delegate/DelegatorFactory.sol index 4f149bab..1f524e4b 100644 --- a/smart-contracts/contracts/delegate/DelegatorFactory.sol +++ b/smart-contracts/contracts/delegate/DelegatorFactory.sol @@ -9,28 +9,26 @@ import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/Upgradeabl import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import {IProvidersDelegator} from "../interfaces/delegate/IProvidersDelegator.sol"; +import {IDelegatorFactory} from "../interfaces/delegate/IDelegatorFactory.sol"; import {IOwnable} from "../interfaces/utils/IOwnable.sol"; -contract DelegatorFactory is OwnableUpgradeable, PausableUpgradeable, UUPSUpgradeable { +contract DelegatorFactory is IDelegatorFactory, OwnableUpgradeable, PausableUpgradeable, UUPSUpgradeable { address public lumerinDiamond; address public beacon; mapping(address => address[]) public proxies; - event ProxyDeployed(address indexed proxyAddress); - event ImplementationUpdated(address indexed newImplementation); - constructor() { _disableInitializers(); } - function DelegatorFactory_init(address _lumerinDiamond, address _implementation) external initializer { + function DelegatorFactory_init(address lumerinDiamond_, address implementation_) external initializer { __Pausable_init(); __Ownable_init(); __UUPSUpgradeable_init(); - lumerinDiamond = _lumerinDiamond; + lumerinDiamond = lumerinDiamond_; - beacon = address(new UpgradeableBeacon(_implementation)); + beacon = address(new UpgradeableBeacon(implementation_)); } function pause() external onlyOwner { @@ -45,23 +43,33 @@ contract DelegatorFactory is OwnableUpgradeable, PausableUpgradeable, UUPSUpgrad address feeTreasury_, uint256 fee_, string memory name_, - string memory endpoint_ + string memory endpoint_, + uint128 deregistrationTimeout_, + uint128 deregistrationNonFeePeriod_ ) external whenNotPaused returns (address) { bytes32 salt_ = _calculatePoolSalt(_msgSender()); address proxy_ = address(new BeaconProxy{salt: salt_}(beacon, bytes(""))); - proxies[_msgSender()].push(address(proxy_)); + proxies[_msgSender()].push(proxy_); - IProvidersDelegator(proxy_).ProvidersDelegator_init(lumerinDiamond, feeTreasury_, fee_, name_, endpoint_); + IProvidersDelegator(proxy_).ProvidersDelegator_init( + lumerinDiamond, + feeTreasury_, + fee_, + name_, + endpoint_, + deregistrationTimeout_, + deregistrationNonFeePeriod_ + ); IOwnable(proxy_).transferOwnership(_msgSender()); - emit ProxyDeployed(address(proxy_)); + emit ProxyDeployed(proxy_); - return address(proxy_); + return proxy_; } - function predictProxyAddress(address _deployer) external view returns (address) { - bytes32 salt_ = _calculatePoolSalt(_deployer); + function predictProxyAddress(address deployer_) external view returns (address) { + bytes32 salt_ = _calculatePoolSalt(deployer_); bytes32 bytecodeHash_ = keccak256( abi.encodePacked(type(BeaconProxy).creationCode, abi.encode(address(beacon), bytes(""))) @@ -70,10 +78,8 @@ contract DelegatorFactory is OwnableUpgradeable, PausableUpgradeable, UUPSUpgrad return Create2.computeAddress(salt_, bytecodeHash_); } - function updateImplementation(address _newImplementation) external onlyOwner { - UpgradeableBeacon(beacon).upgradeTo(_newImplementation); - - emit ImplementationUpdated(_newImplementation); + function updateImplementation(address newImplementation_) external onlyOwner { + UpgradeableBeacon(beacon).upgradeTo(newImplementation_); } function version() external pure returns (uint256) { diff --git a/smart-contracts/contracts/delegate/ProvidersDelegator.sol b/smart-contracts/contracts/delegate/ProvidersDelegator.sol index 6f09a9e7..5fde9a14 100644 --- a/smart-contracts/contracts/delegate/ProvidersDelegator.sol +++ b/smart-contracts/contracts/delegate/ProvidersDelegator.sol @@ -16,34 +16,26 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { using SafeERC20 for IERC20; using Math for uint256; - /** - * @dev The Lumerin protocol data - */ address public lumerinDiamond; address public token; - /** - * @dev The fee data - */ address public feeTreasury; uint256 public fee; - /** - * @dev Provider metadata - */ string public name; string public endpoint; - - /** - * @dev The main contract logic data - */ uint256 public totalStaked; uint256 public totalRate; uint256 public lastContractBalance; bool public isStakeClosed; mapping(address => Staker) public stakers; + uint128 public deregistrationOpenAt; + uint128 public deregistrationTimeout; + uint128 public deregistrationNonFeeOpened; + uint128 public deregistrationNonFeePeriod; + constructor() { _disableInitializers(); } @@ -53,7 +45,9 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { address feeTreasury_, uint256 fee_, string memory name_, - string memory endpoint_ + string memory endpoint_, + uint128 deregistrationTimeout_, + uint128 deregistrationNonFeePeriod_ ) external initializer { __Ownable_init(); @@ -62,9 +56,18 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { setName(name_); setEndpoint(endpoint_); - setFee(feeTreasury_, fee_); + setFeeTreasury(feeTreasury_); + + if (fee_ > PRECISION) { + revert InvalidFee(fee_, PRECISION); + } + fee = fee_; IERC20(token).approve(lumerinDiamond_, type(uint256).max); + + deregistrationTimeout = deregistrationTimeout_; + deregistrationOpenAt = uint128(block.timestamp) + deregistrationTimeout_; + deregistrationNonFeePeriod = deregistrationNonFeePeriod_; } function setName(string memory name_) public onlyOwner { @@ -87,18 +90,14 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { emit EndpointUpdated(endpoint_); } - function setFee(address feeTreasury_, uint256 fee_) public onlyOwner { + function setFeeTreasury(address feeTreasury_) public onlyOwner { if (feeTreasury_ == address(0)) { revert InvalidFeeTreasuryAddress(); } - if (fee_ > PRECISION) { - revert InvalidFee(fee_, PRECISION); - } - fee = fee_; feeTreasury = feeTreasury_; - emit FeeUpdated(fee_, feeTreasury_); + emit FeeTreasuryUpdated(feeTreasury_); } function setIsStakeClosed(bool isStakeClosed_) public onlyOwner { @@ -117,10 +116,11 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { if (isStakeClosed) { revert StakeClosed(); } + if (amount_ == 0) { revert InsufficientAmount(); } - + address user_ = _msgSender(); Staker storage staker = stakers[user_]; @@ -168,6 +168,7 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { staker.rate = currentRate_; staker.staked += amount_; + staker.claimed += amount_; staker.pendingRewards = pendingRewards_ - amount_; IProviderRegistry(lumerinDiamond).providerRegister(address(this), amount_, endpoint); @@ -192,9 +193,10 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { staker.rate = currentRate_; staker.pendingRewards = pendingRewards_ - amount_; + staker.claimed += amount_; uint256 feeAmount_ = (amount_ * fee) / PRECISION; - if (feeAmount_ != 0) { + if (feeAmount_ != 0 && block.timestamp > deregistrationNonFeeOpened + deregistrationNonFeePeriod) { IERC20(token).safeTransfer(feeTreasury, feeAmount_); amount_ -= feeAmount_; @@ -221,25 +223,42 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { } function getCurrentStakerRewards(address staker_) public view returns (uint256) { - Staker memory staker = stakers[staker_]; - (uint256 currentRate_,) = getCurrentRate(); + (uint256 currentRate_, ) = getCurrentRate(); - return _getCurrentStakerRewards(currentRate_, staker); + return _getCurrentStakerRewards(currentRate_, stakers[staker_]); } - function providerDeregister() external onlyOwner { + function providerDeregister(bytes32[] calldata bidIds_) external { + if (block.timestamp < deregistrationOpenAt) { + _checkOwner(); + } else { + deregistrationOpenAt = uint128(block.timestamp) + deregistrationTimeout; + } + + _deleteModelBids(bidIds_); IProviderRegistry(lumerinDiamond).providerDeregister(address(this)); + + deregistrationNonFeeOpened = uint128(block.timestamp); } - function postModelBid( - bytes32 modelId_, - uint256 pricePerSecond_ - ) external onlyOwner returns (bytes32) { + function postModelBid(bytes32 modelId_, uint256 pricePerSecond_) external onlyOwner returns (bytes32) { return IMarketplace(lumerinDiamond).postModelBid(address(this), modelId_, pricePerSecond_); } - function deleteModelBid(bytes32 bidId_) external onlyOwner { - return IMarketplace(lumerinDiamond).deleteModelBid(bidId_); + function deleteModelBids(bytes32[] calldata bidIds_) external { + if (block.timestamp < deregistrationOpenAt) { + _checkOwner(); + } + + _deleteModelBids(bidIds_); + } + + function _deleteModelBids(bytes32[] calldata bidIds_) private { + address lumerinDiamond_ = lumerinDiamond; + + for (uint256 i = 0; i < bidIds_.length; i++) { + IMarketplace(lumerinDiamond_).deleteModelBid(bidIds_[i]); + } } function _getCurrentStakerRewards(uint256 delegatorRate_, Staker memory staker_) private pure returns (uint256) { diff --git a/smart-contracts/contracts/interfaces/delegate/IDelegatorFactory.sol b/smart-contracts/contracts/interfaces/delegate/IDelegatorFactory.sol new file mode 100644 index 00000000..c95d24dc --- /dev/null +++ b/smart-contracts/contracts/interfaces/delegate/IDelegatorFactory.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IDelegatorFactory { + event ProxyDeployed(address indexed proxyAddress); + event ImplementationUpdated(address indexed newImplementation); + + /** + * The function to initialize the contract. + * @param lumerinDiamond_ The Lumerin protocol address. + * @param implementation_ The implementation address. + */ + function DelegatorFactory_init(address lumerinDiamond_, address implementation_) external; + + /** + * Triggers stopped state. + */ + function pause() external; + + /** + * Returns to normal state. + */ + function unpause() external; + + /** + * The function to deploy the new proxy contract. + * @param feeTreasury_ The subnet fee treasury. + * @param fee_ The fee percent where 100% = 10^25. + * @param name_ The Subnet name. + * @param endpoint_ The subnet endpoint. + * @param deregistrationTimeout_ Provider deregistration will be available after this timeout. + * @param deregistrationNonFeePeriod_ Period after deregistration when Stakers can claim rewards without fee. + */ + function deployProxy( + address feeTreasury_, + uint256 fee_, + string memory name_, + string memory endpoint_, + uint128 deregistrationTimeout_, + uint128 deregistrationNonFeePeriod_ + ) external returns (address); + + /** + * The function to predict new proxy address. + * @param _deployer The deployer address. + */ + function predictProxyAddress(address _deployer) external view returns (address); + + /** + * The function to upgrade the implementation. + * @param _newImplementation The new implementation address. + */ + function updateImplementation(address _newImplementation) external; + + /** + * The function to get contract version. + */ + function version() external pure returns (uint256); +} diff --git a/smart-contracts/contracts/interfaces/delegate/IProvidersDelegator.sol b/smart-contracts/contracts/interfaces/delegate/IProvidersDelegator.sol index 5843a4ec..84bd7fe8 100644 --- a/smart-contracts/contracts/interfaces/delegate/IProvidersDelegator.sol +++ b/smart-contracts/contracts/interfaces/delegate/IProvidersDelegator.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.24; interface IProvidersDelegator { event NameUpdated(string name); event EndpointUpdated(string endpoint); - event FeeUpdated(uint256 fee, address feeTreasury); + event FeeTreasuryUpdated(address feeTreasury); event IsStakeClosedUpdated(bool isStakeClosed); event IsRestakeDisabledUpdated(address staker, bool isRestakeDisabled); event Staked(address staker, uint256 staked, uint256 pendingRewards, uint256 rate); @@ -12,8 +12,6 @@ interface IProvidersDelegator { event Claimed(address staker, uint256 staked, uint256 pendingRewards, uint256 rate); event FeeClaimed(address feeTreasury, uint256 feeAmount); - - error InvalidNameLength(); error InvalidEndpointLength(); error InvalidFeeTreasuryAddress(); @@ -24,49 +22,123 @@ interface IProvidersDelegator { error RestakeInvalidCaller(address caller, address staker); error ClaimAmountIsZero(); + /** + * @param staked Staked amount. + * @param claimed Claimed amount. + * @param rate The user internal rate. + * @param pendingRewards Pending rewards for claim. + * @param isRestakeDisabled If true, restake isn't available. + */ struct Staker { uint256 staked; + uint256 claimed; uint256 rate; uint256 pendingRewards; bool isRestakeDisabled; } + /** + * The function to initialize the contract. + * @param lumerinDiamond_ The Lumerin protocol address. + * @param feeTreasury_ The subnet fee treasury. + * @param fee_ The fee percent where 100% = 10^25. + * @param name_ The Subnet name. + * @param endpoint_ The subnet endpoint. + * @param deregistrationTimeout_ Provider deregistration will be available after this timeout. + * @param deregistrationNonFeePeriod_ Period after deregistration when Stakers can claim rewards without fee. + */ function ProvidersDelegator_init( address lumerinDiamond_, address feeTreasury_, uint256 fee_, string memory name_, - string memory endpoint_ + string memory endpoint_, + uint128 deregistrationTimeout_, + uint128 deregistrationNonFeePeriod_ ) external; + /** + * The function to set the Subnet name. + * @param name_ New name. + */ function setName(string memory name_) external; + /** + * The function to set the new endpoint. + * @param endpoint_ New endpoint. + */ function setEndpoint(string memory endpoint_) external; - function setFee(address feeTreasury_, uint256 fee_) external; + /** + * The function to set fee treasury address. + * @param feeTreasury_ New address + */ + function setFeeTreasury(address feeTreasury_) external; + /** + * The function close or open possibility to stake new tokens. + * @param isStakeClosed_ True or False. + */ function setIsStakeClosed(bool isStakeClosed_) external; + /** + * The function to disabled possibility for restake. + * @param isRestakeDisabled_ True or False. + */ function setIsRestakeDisabled(bool isRestakeDisabled_) external; + /** + * The function to stake tokens. + * @param amount_ Amount to stake. + */ function stake(uint256 amount_) external; + /** + * The function to restake rewards. + * @param staker_ Staker address. + * @param amount_ Amount to stake. + */ function restake(address staker_, uint256 amount_) external; + /** + * The function to claim rewards. + * @param staker_ Staker address. + * @param amount_ Amount to stake. + */ function claim(address staker_, uint256 amount_) external; + /** + * The function to return the current rate. + */ function getCurrentRate() external view returns (uint256, uint256); + /** + * The function to get amount of the Staker rewards. + * @param staker_ Staker address. + */ function getCurrentStakerRewards(address staker_) external view returns (uint256); - function providerDeregister() external; - - function postModelBid( - bytes32 modelId_, - uint256 pricePerSecond_ - ) external returns (bytes32); - - function deleteModelBid(bytes32 bidId_) external; - + /** + * The function to deregister the provider. + * @param bidIds_ Bid IDs. + */ + function providerDeregister(bytes32[] calldata bidIds_) external; + + /** + * The function create the model bid. + * @param modelId_ Model ID. + * @param pricePerSecond_ Price per second. + */ + function postModelBid(bytes32 modelId_, uint256 pricePerSecond_) external returns (bytes32); + + /** + * The function to delete model bids. + * @param bidIds_ Bid IDs. + */ + function deleteModelBids(bytes32[] calldata bidIds_) external; + + /** + * The function to get contract version. + */ function version() external pure returns (uint256); } diff --git a/smart-contracts/test/delegate/DelegatorFactory.test.ts b/smart-contracts/test/delegate/DelegatorFactory.test.ts index 92728733..2715750d 100644 --- a/smart-contracts/test/delegate/DelegatorFactory.test.ts +++ b/smart-contracts/test/delegate/DelegatorFactory.test.ts @@ -87,7 +87,7 @@ describe('DelegatorFactory', () => { }); it('should deploy a new proxy', async () => { - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); const proxy = providersDelegatorFactory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegator; @@ -98,9 +98,9 @@ describe('DelegatorFactory', () => { expect(await proxy.endpoint()).to.eq('endpoint'); }); it('should deploy new proxies', async () => { - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1'); - await delegatorFactory.connect(SHEV).deployProxy(SHEV, wei(0.2, 25), 'name2', 'endpoint2'); - await delegatorFactory.connect(KYLE).deployProxy(SHEV, wei(0.3, 25), 'name3', 'endpoint3'); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 1, 2); + await delegatorFactory.connect(SHEV).deployProxy(SHEV, wei(0.2, 25), 'name2', 'endpoint2', 1, 2); + await delegatorFactory.connect(KYLE).deployProxy(SHEV, wei(0.3, 25), 'name3', 'endpoint3', 1, 2); let proxy = providersDelegatorFactory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegator; expect(await proxy.owner()).to.eq(SHEV); @@ -120,11 +120,11 @@ describe('DelegatorFactory', () => { it('should revert when paused and not after the unpause', async () => { await delegatorFactory.pause(); await expect( - delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1'), + delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 1, 2), ).to.be.rejectedWith('Pausable: paused'); await delegatorFactory.unpause(); - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1'); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 1, 2); }); it('should throw error when caller is not an owner', async () => { await expect(delegatorFactory.connect(KYLE).pause()).to.be.revertedWith('Ownable: caller is not the owner'); @@ -138,7 +138,7 @@ describe('DelegatorFactory', () => { describe('#predictProxyAddress', () => { it('should predict a proxy address', async () => { const predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); const proxyAddress = await delegatorFactory.proxies(SHEV, 0); @@ -146,28 +146,28 @@ describe('DelegatorFactory', () => { }); it('should predict proxy addresses', async () => { let predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); expect(await delegatorFactory.proxies(SHEV, 0)).to.eq(predictedProxyAddress); predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); expect(await delegatorFactory.proxies(SHEV, 1)).to.eq(predictedProxyAddress); predictedProxyAddress = await delegatorFactory.predictProxyAddress(KYLE); - await delegatorFactory.connect(KYLE).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + await delegatorFactory.connect(KYLE).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); expect(await delegatorFactory.proxies(KYLE, 0)).to.eq(predictedProxyAddress); predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); expect(await delegatorFactory.proxies(SHEV, 2)).to.eq(predictedProxyAddress); }); }); describe('#updateImplementation', () => { it('should update proxies implementation', async () => { - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); - await delegatorFactory.connect(KYLE).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint'); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); + await delegatorFactory.connect(KYLE).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); const factory = await ethers.getContractFactory('UUPSMock'); const newImpl = await factory.deploy(); diff --git a/smart-contracts/test/delegate/ProviderDelegator.test.ts b/smart-contracts/test/delegate/ProviderDelegator.test.ts index 2a191b63..993d26b4 100644 --- a/smart-contracts/test/delegate/ProviderDelegator.test.ts +++ b/smart-contracts/test/delegate/ProviderDelegator.test.ts @@ -53,6 +53,7 @@ describe('ProvidersDelegator', () => { let delegateRegistry: DelegateRegistry; before(async () => { + // await setTime(5000); [OWNER, DELEGATOR, TREASURY, KYLE, SHEV, ALAN] = await ethers.getSigners(); [diamond, token, delegateRegistry] = await Promise.all([ @@ -75,6 +76,8 @@ describe('ProvidersDelegator', () => { wei(0.2, 25), 'DLNAME', 'ENDPOINT', + 3600, + 300, ); await token.transfer(KYLE, wei(1000)); @@ -86,6 +89,7 @@ describe('ProvidersDelegator', () => { await token.connect(ALAN).approve(sessionRouter, wei(1000)); await token.connect(KYLE).approve(providersDelegator, wei(1000)); await token.connect(SHEV).approve(providersDelegator, wei(1000)); + await token.connect(ALAN).approve(providersDelegator, wei(1000)); await token.connect(DELEGATOR).approve(modelRegistry, wei(1000)); await reverter.snapshot(); @@ -96,9 +100,14 @@ describe('ProvidersDelegator', () => { describe('#ProvidersDelegator_init', () => { it('should revert if try to call init function twice', async () => { await expect( - providersDelegator.ProvidersDelegator_init(OWNER, await TREASURY.getAddress(), 1, '', ''), + providersDelegator.ProvidersDelegator_init(OWNER, await TREASURY.getAddress(), 1, '', '', 0, 0), ).to.be.rejectedWith('Initializable: contract is already initialized'); }); + it('should throw error when fee is invalid', async () => { + await expect( + deployProvidersDelegator(diamond, await TREASURY.getAddress(), wei(1.1, 25), 'DLNAME', 'ENDPOINT', 3600, 300), + ).to.be.revertedWithCustomError(providersDelegator, 'InvalidFee'); + }); }); describe('#setName', () => { @@ -137,27 +146,20 @@ describe('ProvidersDelegator', () => { }); }); - describe('#setFee', () => { + describe('#setFeeTreasuryTreasury', () => { it('should set the provider fee', async () => { - await providersDelegator.setFee(KYLE, wei(0.1, 25)); + await providersDelegator.setFeeTreasury(KYLE); - expect(await providersDelegator.fee()).eq(wei(0.1, 25)); expect(await providersDelegator.feeTreasury()).eq(KYLE); }); it('should throw error when fee treasury is invalid', async () => { - await expect(providersDelegator.setFee(ZERO_ADDR, wei(0.1, 25))).to.be.revertedWithCustomError( + await expect(providersDelegator.setFeeTreasury(ZERO_ADDR)).to.be.revertedWithCustomError( providersDelegator, 'InvalidFeeTreasuryAddress', ); }); - it('should throw error when fee is invalid', async () => { - await expect(providersDelegator.setFee(KYLE, wei(1.01, 25))).to.be.revertedWithCustomError( - providersDelegator, - 'InvalidFee', - ); - }); it('should throw error when caller is not an owner', async () => { - await expect(providersDelegator.connect(KYLE).setFee(KYLE, wei(1.01, 25))).to.be.revertedWith( + await expect(providersDelegator.connect(KYLE).setFeeTreasury(KYLE)).to.be.revertedWith( 'Ownable: caller is not the owner', ); }); @@ -229,14 +231,15 @@ describe('ProvidersDelegator', () => { describe('#claim', () => { beforeEach(async () => { - await providersDelegator.setFee(TREASURY, wei(0.2, 25)); + await setTime(5000); }); - it('should correctly claim, one staker, full claim', async () => { await providersDelegator.connect(KYLE).stake(wei(100)); await token.transfer(providersDelegator, wei(10)); + expect(await providersDelegator.getCurrentStakerRewards(KYLE)).to.eq(wei(10)); + await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); expect(await token.balanceOf(KYLE)).to.eq(wei(908)); expect(await token.balanceOf(TREASURY)).to.eq(wei(2)); @@ -303,19 +306,6 @@ describe('ProvidersDelegator', () => { expect(await token.balanceOf(SHEV)).to.eq(wei(784)); expect(await token.balanceOf(TREASURY)).to.eq(wei(4 + 4 + 4 + 5 + 13)); }); - it('should correctly claim, full amount without fee', async () => { - await providersDelegator.setFee(TREASURY, wei(0, 25)); - - await providersDelegator.connect(KYLE).stake(wei(100)); - expect(await providersDelegator.getCurrentStakerRewards(KYLE)).to.eq(wei(0)); - - await token.transfer(providersDelegator, wei(10)); - expect(await providersDelegator.getCurrentStakerRewards(KYLE)).to.eq(wei(10)); - - await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); - expect(await token.balanceOf(KYLE)).to.eq(wei(910)); - expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); - }); it('should throw error when nothing to claim', async () => { await expect(providersDelegator.connect(KYLE).claim(KYLE, wei(999))).to.be.revertedWithCustomError( providersDelegator, @@ -326,9 +316,8 @@ describe('ProvidersDelegator', () => { describe('#restake', () => { beforeEach(async () => { - await providersDelegator.setFee(TREASURY, wei(0.2, 25)); + await setTime(5000); }); - it('should correctly restake, two stakers, full restake', async () => { await providersDelegator.connect(KYLE).stake(wei(100)); await providersDelegator.connect(SHEV).stake(wei(300)); @@ -397,26 +386,22 @@ describe('ProvidersDelegator', () => { }); describe('#providerDeregister', () => { - beforeEach(async () => { - await providersDelegator.setFee(TREASURY, wei(0.2, 25)); - }); - it('should deregister the provider', async () => { await providersDelegator.connect(KYLE).stake(wei(100)); - await providersDelegator.providerDeregister(); + await providersDelegator.providerDeregister([]); await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); - expect(await token.balanceOf(KYLE)).to.eq(wei(980)); - expect(await token.balanceOf(TREASURY)).to.eq(wei(20)); + expect(await token.balanceOf(KYLE)).to.eq(wei(1000)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); }); it('should throw error when caller is not an owner', async () => { - await expect(providersDelegator.connect(KYLE).providerDeregister()).to.be.revertedWith( + await expect(providersDelegator.connect(KYLE).providerDeregister([])).to.be.revertedWith( 'Ownable: caller is not the owner', ); }); }); - describe('#postModelBid, #deleteModelBid', () => { + describe('#postModelBid, #deleteModelBids', () => { const baseModelId = getHex(Buffer.from('1')); it('should deregister the model bid and delete it', async () => { @@ -433,9 +418,16 @@ describe('ProvidersDelegator', () => { // Register bid await providersDelegator.postModelBid(modelId, wei(0.0001)); - const bidId = await marketplace.getBidId(await providersDelegator.getAddress(), modelId, 0); + let bidId = await marketplace.getBidId(await providersDelegator.getAddress(), modelId, 0); + + await providersDelegator.deleteModelBids([bidId]); + + // Register bid again and deregister not from OWNER + await providersDelegator.postModelBid(modelId, wei(0.0001)); + bidId = await marketplace.getBidId(await providersDelegator.getAddress(), modelId, 1); - await providersDelegator.deleteModelBid(bidId); + await setTime(10000); + await providersDelegator.connect(ALAN).deleteModelBids([bidId]); }); it('should throw error when caller is not an owner', async () => { await expect(providersDelegator.connect(KYLE).postModelBid(baseModelId, wei(0.0001))).to.be.revertedWith( @@ -443,7 +435,7 @@ describe('ProvidersDelegator', () => { ); }); it('should throw error when caller is not an owner', async () => { - await expect(providersDelegator.connect(KYLE).deleteModelBid(baseModelId)).to.be.revertedWith( + await expect(providersDelegator.connect(KYLE).deleteModelBids([baseModelId])).to.be.revertedWith( 'Ownable: caller is not the owner', ); }); @@ -458,9 +450,8 @@ describe('ProvidersDelegator', () => { describe('full flow', () => { const baseModelId = getHex(Buffer.from('1')); - it('should deregister the provider', async () => { + it('should claim correct reward amount', async () => { // Register provider - await providersDelegator.setFee(TREASURY, wei(0.2, 25)); await providersDelegator.connect(KYLE).stake(wei(100)); await providersDelegator.connect(SHEV).stake(wei(300)); @@ -497,6 +488,53 @@ describe('ProvidersDelegator', () => { expect(await token.balanceOf(SHEV)).to.eq(wei(700) + BigInt(Number(reward.toString()) * 0.75 * 0.8)); expect(await token.balanceOf(TREASURY)).to.eq(BigInt(Number(reward.toString()) * 0.2)); }); + + it('should correctly deregister provider without fees', async () => { + await setTime(payoutStart + 1 * DAY); + + // Register provider + await providersDelegator.connect(KYLE).stake(wei(100)); + await providersDelegator.connect(SHEV).stake(wei(300)); + + // Register model + await modelRegistry + .connect(DELEGATOR) + .modelRegister(DELEGATOR, baseModelId, getHex(Buffer.from('ipfs://ipfsaddress')), 0, wei(100), 'name', [ + 'tag_1', + ]); + const modelId = await modelRegistry.getModelId(DELEGATOR, baseModelId); + + // Register bid + await providersDelegator.postModelBid(modelId, wei(0.0001)); + const bidId = await marketplace.getBidId(await providersDelegator.getAddress(), modelId, 0); + + // Open session + await setTime(payoutStart + 10 * DAY); + const { msg, signature } = await getProviderApproval(OWNER, ALAN, bidId); + await sessionRouter.connect(ALAN).openSession(ALAN, wei(50), false, msg, signature); + const sessionId = await sessionRouter.getSessionId(ALAN, providersDelegator, bidId, 0); + + // Close session + await setTime(payoutStart + 15 * DAY); + const { msg: receiptMsg } = await getReceipt(OWNER, sessionId, 0, 0); + const { signature: receiptSig } = await getReceipt(OWNER, sessionId, 0, 0); + await sessionRouter.connect(ALAN).closeSession(receiptMsg, receiptSig); + + // Add the new Staker + await providersDelegator.connect(ALAN).stake(wei(1000)); + + // Deregister the providers + await providersDelegator.connect(KYLE).providerDeregister([bidId]); + + // Claim rewards + await providersDelegator.claim(KYLE, wei(9999)); + await providersDelegator.claim(SHEV, wei(9999)); + await providersDelegator.claim(ALAN, wei(9999)); + expect(await token.balanceOf(KYLE)).to.closeTo(wei(1000), wei(0.1)); + expect(await token.balanceOf(SHEV)).to.closeTo(wei(1000), wei(0.1)); + expect(await token.balanceOf(ALAN)).to.closeTo(wei(1000), wei(0.2)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); + }); }); }); diff --git a/smart-contracts/test/helpers/deployers/delegate/providers-delegator.ts b/smart-contracts/test/helpers/deployers/delegate/providers-delegator.ts index d2d6ce60..41d64de5 100644 --- a/smart-contracts/test/helpers/deployers/delegate/providers-delegator.ts +++ b/smart-contracts/test/helpers/deployers/delegate/providers-delegator.ts @@ -9,6 +9,8 @@ export const deployProvidersDelegator = async ( fee: BigNumberish, name: string, endpoint: string, + deregistrationTimeout: number, + deregistrationNonFeePeriod: number, ): Promise => { const [implFactory, proxyFactory] = await Promise.all([ ethers.getContractFactory('ProvidersDelegator'), @@ -19,7 +21,15 @@ export const deployProvidersDelegator = async ( const proxy = await proxyFactory.deploy(impl, '0x'); const contract = implFactory.attach(proxy) as ProvidersDelegator; - await contract.ProvidersDelegator_init(diamond, feeTreasury, fee, name, endpoint); + await contract.ProvidersDelegator_init( + diamond, + feeTreasury, + fee, + name, + endpoint, + deregistrationTimeout, + deregistrationNonFeePeriod, + ); return contract; }; From bb46e569535b5fe418be469b5ca6ed1026cb90ba Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Mon, 16 Dec 2024 12:36:11 +0200 Subject: [PATCH 05/14] delegate review fixes --- .../contracts/delegate/ProvidersDelegator.sol | 5 +++++ .../interfaces/delegate/IDelegatorFactory.sol | 16 ++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/smart-contracts/contracts/delegate/ProvidersDelegator.sol b/smart-contracts/contracts/delegate/ProvidersDelegator.sol index 5fde9a14..c02d1387 100644 --- a/smart-contracts/contracts/delegate/ProvidersDelegator.sol +++ b/smart-contracts/contracts/delegate/ProvidersDelegator.sol @@ -16,21 +16,26 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { using SafeERC20 for IERC20; using Math for uint256; + // Deps address public lumerinDiamond; address public token; + // Fee address public feeTreasury; uint256 public fee; + // Metadata string public name; string public endpoint; + // Main calculation storage uint256 public totalStaked; uint256 public totalRate; uint256 public lastContractBalance; bool public isStakeClosed; mapping(address => Staker) public stakers; + // Deregistration limits uint128 public deregistrationOpenAt; uint128 public deregistrationTimeout; uint128 public deregistrationNonFeeOpened; diff --git a/smart-contracts/contracts/interfaces/delegate/IDelegatorFactory.sol b/smart-contracts/contracts/interfaces/delegate/IDelegatorFactory.sol index c95d24dc..4f059c1f 100644 --- a/smart-contracts/contracts/interfaces/delegate/IDelegatorFactory.sol +++ b/smart-contracts/contracts/interfaces/delegate/IDelegatorFactory.sol @@ -2,8 +2,11 @@ pragma solidity ^0.8.24; interface IDelegatorFactory { + /** + * The event that is emitted when the proxy deployed. + * @param proxyAddress The pool's id. + */ event ProxyDeployed(address indexed proxyAddress); - event ImplementationUpdated(address indexed newImplementation); /** * The function to initialize the contract. @@ -30,6 +33,7 @@ interface IDelegatorFactory { * @param endpoint_ The subnet endpoint. * @param deregistrationTimeout_ Provider deregistration will be available after this timeout. * @param deregistrationNonFeePeriod_ Period after deregistration when Stakers can claim rewards without fee. + * @return Deployed proxy address */ function deployProxy( address feeTreasury_, @@ -42,18 +46,18 @@ interface IDelegatorFactory { /** * The function to predict new proxy address. - * @param _deployer The deployer address. + * @param deployer_ The deployer address. */ - function predictProxyAddress(address _deployer) external view returns (address); + function predictProxyAddress(address deployer_) external view returns (address); /** * The function to upgrade the implementation. - * @param _newImplementation The new implementation address. + * @param newImplementation_ The new implementation address. */ - function updateImplementation(address _newImplementation) external; + function updateImplementation(address newImplementation_) external; /** - * The function to get contract version. + * @return The contract version. */ function version() external pure returns (uint256); } From 8218533c4190e847ee71bf87b489f078597c1386 Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Mon, 16 Dec 2024 13:51:33 +0200 Subject: [PATCH 06/14] add fee for the restake --- .../contracts/delegate/ProvidersDelegator.sol | 16 +++++++--- .../test/delegate/ProviderDelegator.test.ts | 32 +++++++++++-------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/smart-contracts/contracts/delegate/ProvidersDelegator.sol b/smart-contracts/contracts/delegate/ProvidersDelegator.sol index c02d1387..3fc0c287 100644 --- a/smart-contracts/contracts/delegate/ProvidersDelegator.sol +++ b/smart-contracts/contracts/delegate/ProvidersDelegator.sol @@ -166,18 +166,26 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { revert InsufficientAmount(); } + uint256 feeAmount_ = (amount_ * fee) / PRECISION; + uint256 amountWithFee_ = amount_ - feeAmount_; + if (feeAmount_ != 0) { + IERC20(token).safeTransfer(feeTreasury, feeAmount_); + + emit FeeClaimed(feeTreasury, feeAmount_); + } + + IProviderRegistry(lumerinDiamond).providerRegister(address(this), amountWithFee_, endpoint); + totalRate = currentRate_; - totalStaked += amount_; + totalStaked += amountWithFee_; lastContractBalance = contractBalance_ - amount_; staker.rate = currentRate_; - staker.staked += amount_; + staker.staked += amountWithFee_; staker.claimed += amount_; staker.pendingRewards = pendingRewards_ - amount_; - IProviderRegistry(lumerinDiamond).providerRegister(address(this), amount_, endpoint); - emit Restaked(staker_, staker.staked, staker.pendingRewards, staker.rate); } diff --git a/smart-contracts/test/delegate/ProviderDelegator.test.ts b/smart-contracts/test/delegate/ProviderDelegator.test.ts index 993d26b4..09630af4 100644 --- a/smart-contracts/test/delegate/ProviderDelegator.test.ts +++ b/smart-contracts/test/delegate/ProviderDelegator.test.ts @@ -325,21 +325,20 @@ describe('ProvidersDelegator', () => { await token.transfer(providersDelegator, wei(100)); await providersDelegator.connect(OWNER).restake(KYLE, wei(9999)); - expect((await providersDelegator.stakers(KYLE)).staked).to.eq(wei(125)); + expect((await providersDelegator.stakers(KYLE)).staked).to.eq(wei(120)); expect(await token.balanceOf(KYLE)).to.eq(wei(900)); - expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(5)); await token.transfer(providersDelegator, wei(100)); await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); - expect(await token.balanceOf(KYLE)).to.closeTo(wei(900 + 29.41 * 0.8), wei(0.01)); - expect(await token.balanceOf(TREASURY)).to.closeTo(wei(29.41 * 0.2), wei(0.01)); + expect(await token.balanceOf(KYLE)).to.closeTo(wei(900 + 28.57 * 0.8), wei(0.01)); + expect(await token.balanceOf(TREASURY)).to.closeTo(wei(5 + 28.57 * 0.2), wei(0.01)); await providersDelegator.connect(SHEV).claim(SHEV, wei(9999)); - expect(await token.balanceOf(SHEV)).to.closeTo(wei(700 + 75 * 0.8 + 70.58 * 0.8), wei(0.01)); - expect(await token.balanceOf(TREASURY)).to.closeTo(wei(29.41 * 0.2 + 75 * 0.2 + 70.58 * 0.2), wei(0.01)); + expect(await token.balanceOf(SHEV)).to.closeTo(wei(700 + 75 * 0.8 + 71.42 * 0.8), wei(0.01)); + expect(await token.balanceOf(TREASURY)).to.closeTo(wei(5 + 28.57 * 0.2 + 75 * 0.2 + 71.42 * 0.2), wei(0.01)); }); - it('should correctly restake, two stakers, partial restake', async () => { await providersDelegator.connect(KYLE).stake(wei(100)); await providersDelegator.connect(SHEV).stake(wei(300)); @@ -347,23 +346,30 @@ describe('ProvidersDelegator', () => { await token.transfer(providersDelegator, wei(100)); await providersDelegator.connect(OWNER).restake(KYLE, wei(20)); - expect((await providersDelegator.stakers(KYLE)).staked).to.eq(wei(120)); + expect((await providersDelegator.stakers(KYLE)).staked).to.eq(wei(116)); expect(await token.balanceOf(KYLE)).to.eq(wei(900)); - expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(4)); await token.transfer(providersDelegator, wei(100)); await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); - expect(await token.balanceOf(KYLE)).to.closeTo(wei(900 + 5 * 0.8 + 28.57 * 0.8), wei(0.01)); - expect(await token.balanceOf(TREASURY)).to.closeTo(wei(5 * 0.2 + 28.57 * 0.2), wei(0.01)); + expect(await token.balanceOf(KYLE)).to.closeTo(wei(900 + 5 * 0.8 + 27.88 * 0.8), wei(0.01)); + expect(await token.balanceOf(TREASURY)).to.closeTo(wei(4 + 5 * 0.2 + 27.88 * 0.2), wei(0.01)); await providersDelegator.connect(SHEV).claim(SHEV, wei(9999)); - expect(await token.balanceOf(SHEV)).to.closeTo(wei(700 + 75 * 0.8 + 71.42 * 0.8), wei(0.01)); + expect(await token.balanceOf(SHEV)).to.closeTo(wei(700 + 75 * 0.8 + 72.11 * 0.8), wei(0.01)); expect(await token.balanceOf(TREASURY)).to.closeTo( - wei(5 * 0.2 + 28.57 * 0.2 + 75 * 0.2 + 71.42 * 0.2), + wei(4 + 5 * 0.2 + 27.88 * 0.2 + 75 * 0.2 + 72.11 * 0.2), wei(0.01), ); }); + it('should correctly restake with zero fee', async () => { + await providersDelegator.connect(KYLE).stake(wei(100)); + await token.transfer(providersDelegator, wei(10)); + await providersDelegator.connect(OWNER).restake(KYLE, 1); + + expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); + }); it('should throw error when restake caller is invalid', async () => { await expect(providersDelegator.connect(KYLE).restake(SHEV, wei(999))).to.be.revertedWithCustomError( providersDelegator, From 2685c4a064b18f84eb263273244fdc1ed42f7784 Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Mon, 16 Dec 2024 17:50:25 +0200 Subject: [PATCH 07/14] prepare migrations --- .../deploy/2_change_bid_price.migration.ts | 7 +- .../3_delegate_protocol.migration copy.ts | 39 +++++++++++ .../4_update_session_facet.migration.ts | 65 +++++++++++++++++++ .../deploy/data/config_arbitrum_mainnet.json | 1 + .../deploy/data/config_arbitrum_sepolia.json | 1 + .../deploy/helpers/config-parser.ts | 3 +- smart-contracts/hardhat.config.ts | 3 + 7 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 smart-contracts/deploy/3_delegate_protocol.migration copy.ts create mode 100644 smart-contracts/deploy/4_update_session_facet.migration.ts diff --git a/smart-contracts/deploy/2_change_bid_price.migration.ts b/smart-contracts/deploy/2_change_bid_price.migration.ts index 98ad21c9..88ae3bf4 100644 --- a/smart-contracts/deploy/2_change_bid_price.migration.ts +++ b/smart-contracts/deploy/2_change_bid_price.migration.ts @@ -1,10 +1,13 @@ import { Deployer } from '@solarity/hardhat-migrate'; +import { parseConfig } from './helpers/config-parser'; + import { Marketplace__factory } from '@/generated-types/ethers'; module.exports = async function (deployer: Deployer) { - // const marketplaceFacet = await deployer.deployed(Marketplace__factory, '0xb8C55cD613af947E73E262F0d3C54b7211Af16CF'); - const marketplaceFacet = await deployer.deployed(Marketplace__factory, '0xDE819AaEE474626E3f34Ef0263373357e5a6C71b'); + const config = parseConfig(); + + const marketplaceFacet = await deployer.deployed(Marketplace__factory, config.lumerinProtocol); console.log(await marketplaceFacet.getMinMaxBidPricePerSecond()); diff --git a/smart-contracts/deploy/3_delegate_protocol.migration copy.ts b/smart-contracts/deploy/3_delegate_protocol.migration copy.ts new file mode 100644 index 00000000..0e6d452f --- /dev/null +++ b/smart-contracts/deploy/3_delegate_protocol.migration copy.ts @@ -0,0 +1,39 @@ +import { Deployer } from '@solarity/hardhat-migrate'; + +import { parseConfig } from './helpers/config-parser'; + +import { + DelegatorFactory__factory, + ERC1967Proxy__factory, + ProvidersDelegator__factory, +} from '@/generated-types/ethers'; +import { wei } from '@/scripts/utils/utils'; + +module.exports = async function (deployer: Deployer) { + const config = parseConfig(); + + const providersDelegatorImpl = await deployer.deploy(ProvidersDelegator__factory); + const delegatorFactoryImpl = await deployer.deploy(DelegatorFactory__factory); + const proxy = await deployer.deploy(ERC1967Proxy__factory, [await delegatorFactoryImpl.getAddress(), '0x']); + + const delegatorFactory = await deployer.deployed(DelegatorFactory__factory, await proxy.getAddress()); + + await delegatorFactory.DelegatorFactory_init(config.lumerinProtocol, providersDelegatorImpl); + + await delegatorFactory.deployProxy( + '0x19ec1E4b714990620edf41fE28e9a1552953a7F4', + wei(0.2, 25), + 'First Subnet', + 'Custom endpoint', + 60 * 60 * 1, + 60 * 30, + ); +}; + +// npx hardhat migrate --only 3 + +// npx hardhat migrate --network arbitrum_sepolia --only 3 --verify +// npx hardhat migrate --network arbitrum_sepolia --only 3 --verify --continue + +// npx hardhat migrate --network arbitrum --only 3 --verify +// npx hardhat migrate --network arbitrum --only 3 --verify --continue diff --git a/smart-contracts/deploy/4_update_session_facet.migration.ts b/smart-contracts/deploy/4_update_session_facet.migration.ts new file mode 100644 index 00000000..3b638f1c --- /dev/null +++ b/smart-contracts/deploy/4_update_session_facet.migration.ts @@ -0,0 +1,65 @@ +import { Deployer } from '@solarity/hardhat-migrate'; +import { Fragment } from 'ethers'; +import { ethers } from 'hardhat'; + +import { parseConfig } from './helpers/config-parser'; + +import { + ISessionRouter__factory, + IStatsStorage__factory, + LinearDistributionIntervalDecrease__factory, + LumerinDiamond__factory, + SessionRouter__factory, +} from '@/generated-types/ethers'; +import { FacetAction } from '@/test/helpers/deployers'; + +module.exports = async function (deployer: Deployer) { + const config = parseConfig(); + + const ldid = await deployer.deploy(LinearDistributionIntervalDecrease__factory); + const newSessionRouterFacet = await deployer.deploy(SessionRouter__factory, { + libraries: { + LinearDistributionIntervalDecrease: ldid, + }, + }); + + const lumerinDiamond = await deployer.deployed(LumerinDiamond__factory, config.lumerinProtocol); + + // ONLY FOR TESTS + // const testSigner = await ethers.getImpersonatedSigner(await lumerinDiamond.owner()); + // END + + const oldSessionRouterFacet = '0xCc48cB2DbA21A5D36C16f6f64e5B5E138EA1ba13'; + const oldSelectors = await lumerinDiamond.facetFunctionSelectors(oldSessionRouterFacet); + + // ONLY FOR TESTS - remove or add `.connect(testSigner)` + await lumerinDiamond['diamondCut((address,uint8,bytes4[])[])']([ + { + facetAddress: oldSessionRouterFacet, + action: FacetAction.Remove, + functionSelectors: [...oldSelectors], + }, + { + facetAddress: newSessionRouterFacet, + action: FacetAction.Add, + functionSelectors: ISessionRouter__factory.createInterface() + .fragments.filter(Fragment.isFunction) + .map((f) => f.selector), + }, + { + facetAddress: newSessionRouterFacet, + action: FacetAction.Add, + functionSelectors: IStatsStorage__factory.createInterface() + .fragments.filter(Fragment.isFunction) + .map((f) => f.selector), + }, + ]); +}; + +// npx hardhat migrate --only 4 + +// npx hardhat migrate --network arbitrum_sepolia --only 4 --verify +// npx hardhat migrate --network arbitrum_sepolia --only 4 --verify --continue + +// npx hardhat migrate --network arbitrum --only 4 --verify +// npx hardhat migrate --network arbitrum --only 4 --verify --continue diff --git a/smart-contracts/deploy/data/config_arbitrum_mainnet.json b/smart-contracts/deploy/data/config_arbitrum_mainnet.json index f20a0c6a..6eaca754 100644 --- a/smart-contracts/deploy/data/config_arbitrum_mainnet.json +++ b/smart-contracts/deploy/data/config_arbitrum_mainnet.json @@ -8,6 +8,7 @@ "marketplaceMaxBidPricePerSecond": "10000000000000000", "delegateRegistry": "0x00000000000000447e69651d841bD8D104Bed493", "owner": "0x1FE04BC15Cf2c5A2d41a0b3a96725596676eBa1E", + "lumerinProtocol": "0xDE819AaEE474626E3f34Ef0263373357e5a6C71b", "pools": [ { "payoutStart": 1707393600, diff --git a/smart-contracts/deploy/data/config_arbitrum_sepolia.json b/smart-contracts/deploy/data/config_arbitrum_sepolia.json index 097fe54d..97c5be2f 100644 --- a/smart-contracts/deploy/data/config_arbitrum_sepolia.json +++ b/smart-contracts/deploy/data/config_arbitrum_sepolia.json @@ -8,6 +8,7 @@ "marketplaceMaxBidPricePerSecond": "20000000000000000000", "delegateRegistry": "0x00000000000000447e69651d841bD8D104Bed493", "owner": "0x1FE04BC15Cf2c5A2d41a0b3a96725596676eBa1E", + "lumerinProtocol": "0xb8C55cD613af947E73E262F0d3C54b7211Af16CF", "pools": [ { "payoutStart": 1707393600, diff --git a/smart-contracts/deploy/helpers/config-parser.ts b/smart-contracts/deploy/helpers/config-parser.ts index 00c27417..7bd950cd 100644 --- a/smart-contracts/deploy/helpers/config-parser.ts +++ b/smart-contracts/deploy/helpers/config-parser.ts @@ -13,10 +13,11 @@ export type Config = { marketplaceMaxBidPricePerSecond: string; delegateRegistry: string; owner: string; + lumerinProtocol: string; }; export function parseConfig(): Config { - const configPath = `deploy/data/config_arbitrum_mainnet.json`; + const configPath = `deploy/data/config_arbitrum_sepolia.json`; return JSON.parse(readFileSync(configPath, 'utf-8')) as Config; } diff --git a/smart-contracts/hardhat.config.ts b/smart-contracts/hardhat.config.ts index adc7fb4c..7f6235f7 100644 --- a/smart-contracts/hardhat.config.ts +++ b/smart-contracts/hardhat.config.ts @@ -40,6 +40,9 @@ const config: HardhatUserConfig = { // url: `https://arbitrum-sepolia.infura.io/v3/${process.env.INFURA_KEY}`, // }, // forking: { + // url: `https://arb-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`, + // }, + // forking: { // url: `https://arbitrum-mainnet.infura.io/v3/${process.env.INFURA_KEY}`, // }, }, From 288f49cc8a4386cfa6f81c33b1a407682e276346 Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Wed, 18 Dec 2024 22:18:26 +0200 Subject: [PATCH 08/14] add the subnet deregistration end timestamp --- ...legatorFactory.sol => DelegateFactory.sol} | 33 +- ...ersDelegator.sol => ProvidersDelegate.sol} | 109 +++---- ...egatorFactory.sol => IDelegateFactory.sol} | 19 +- ...rsDelegator.sol => IProvidersDelegate.sol} | 81 ++++- ...actory.test.ts => DelegateFactory.test.ts} | 122 ++++--- ...ator.test.ts => ProvidersDelegate.test.ts} | 303 +++++++++--------- .../deployers/delegate/delegate-factory.ts | 23 ++ .../deployers/delegate/delegator-factory.ts | 20 -- .../test/helpers/deployers/delegate/index.ts | 4 +- ...ers-delegator.ts => providers-delegate.ts} | 23 +- 10 files changed, 412 insertions(+), 325 deletions(-) rename smart-contracts/contracts/delegate/{DelegatorFactory.sol => DelegateFactory.sol} (70%) rename smart-contracts/contracts/delegate/{ProvidersDelegator.sol => ProvidersDelegate.sol} (72%) rename smart-contracts/contracts/interfaces/delegate/{IDelegatorFactory.sol => IDelegateFactory.sol} (69%) rename smart-contracts/contracts/interfaces/delegate/{IProvidersDelegator.sol => IProvidersDelegate.sol} (68%) rename smart-contracts/test/delegate/{DelegatorFactory.test.ts => DelegateFactory.test.ts} (64%) rename smart-contracts/test/delegate/{ProviderDelegator.test.ts => ProvidersDelegate.test.ts} (59%) create mode 100644 smart-contracts/test/helpers/deployers/delegate/delegate-factory.ts delete mode 100644 smart-contracts/test/helpers/deployers/delegate/delegator-factory.ts rename smart-contracts/test/helpers/deployers/delegate/{providers-delegator.ts => providers-delegate.ts} (51%) diff --git a/smart-contracts/contracts/delegate/DelegatorFactory.sol b/smart-contracts/contracts/delegate/DelegateFactory.sol similarity index 70% rename from smart-contracts/contracts/delegate/DelegatorFactory.sol rename to smart-contracts/contracts/delegate/DelegateFactory.sol index 1f524e4b..475eeb05 100644 --- a/smart-contracts/contracts/delegate/DelegatorFactory.sol +++ b/smart-contracts/contracts/delegate/DelegateFactory.sol @@ -8,24 +8,31 @@ import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; -import {IProvidersDelegator} from "../interfaces/delegate/IProvidersDelegator.sol"; -import {IDelegatorFactory} from "../interfaces/delegate/IDelegatorFactory.sol"; +import {IProvidersDelegate} from "../interfaces/delegate/IProvidersDelegate.sol"; +import {IDelegateFactory} from "../interfaces/delegate/IDelegateFactory.sol"; import {IOwnable} from "../interfaces/utils/IOwnable.sol"; -contract DelegatorFactory is IDelegatorFactory, OwnableUpgradeable, PausableUpgradeable, UUPSUpgradeable { +contract DelegateFactory is IDelegateFactory, OwnableUpgradeable, PausableUpgradeable, UUPSUpgradeable { address public lumerinDiamond; address public beacon; + mapping(address => address[]) public proxies; + uint128 public minDeregistrationTimeout; constructor() { _disableInitializers(); } - function DelegatorFactory_init(address lumerinDiamond_, address implementation_) external initializer { + function DelegateFactory_init( + address lumerinDiamond_, + address implementation_, + uint128 minDeregistrationTimeout_ + ) external initializer { __Pausable_init(); __Ownable_init(); __UUPSUpgradeable_init(); + setMinDeregistrationTimeout(minDeregistrationTimeout_); lumerinDiamond = lumerinDiamond_; beacon = address(new UpgradeableBeacon(implementation_)); @@ -39,27 +46,35 @@ contract DelegatorFactory is IDelegatorFactory, OwnableUpgradeable, PausableUpgr _unpause(); } + function setMinDeregistrationTimeout(uint128 minDeregistrationTimeout_) public onlyOwner { + minDeregistrationTimeout = minDeregistrationTimeout_; + + emit MinDeregistrationTimeoutUpdated(minDeregistrationTimeout_); + } + function deployProxy( address feeTreasury_, uint256 fee_, string memory name_, string memory endpoint_, - uint128 deregistrationTimeout_, - uint128 deregistrationNonFeePeriod_ + uint128 deregistrationOpenAt ) external whenNotPaused returns (address) { + if (deregistrationOpenAt <= block.timestamp + minDeregistrationTimeout) { + revert InvalidDeregistrationOpenAt(deregistrationOpenAt, uint128(block.timestamp + minDeregistrationTimeout)); + } + bytes32 salt_ = _calculatePoolSalt(_msgSender()); address proxy_ = address(new BeaconProxy{salt: salt_}(beacon, bytes(""))); proxies[_msgSender()].push(proxy_); - IProvidersDelegator(proxy_).ProvidersDelegator_init( + IProvidersDelegate(proxy_).ProvidersDelegate_init( lumerinDiamond, feeTreasury_, fee_, name_, endpoint_, - deregistrationTimeout_, - deregistrationNonFeePeriod_ + deregistrationOpenAt ); IOwnable(proxy_).transferOwnership(_msgSender()); diff --git a/smart-contracts/contracts/delegate/ProvidersDelegator.sol b/smart-contracts/contracts/delegate/ProvidersDelegate.sol similarity index 72% rename from smart-contracts/contracts/delegate/ProvidersDelegator.sol rename to smart-contracts/contracts/delegate/ProvidersDelegate.sol index 3fc0c287..84d6fb86 100644 --- a/smart-contracts/contracts/delegate/ProvidersDelegator.sol +++ b/smart-contracts/contracts/delegate/ProvidersDelegate.sol @@ -7,52 +7,51 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {PRECISION} from "@solarity/solidity-lib/utils/Globals.sol"; -import {IProvidersDelegator} from "../interfaces/delegate/IProvidersDelegator.sol"; +import {IProvidersDelegate} from "../interfaces/delegate/IProvidersDelegate.sol"; import {IBidStorage} from "../interfaces/storage/IBidStorage.sol"; import {IProviderRegistry} from "../interfaces/facets/IProviderRegistry.sol"; import {IMarketplace} from "../interfaces/facets/IMarketplace.sol"; -contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { +contract ProvidersDelegate is IProvidersDelegate, OwnableUpgradeable { using SafeERC20 for IERC20; using Math for uint256; - // Deps + // The contract deps address public lumerinDiamond; address public token; - // Fee + // The owner fee address public feeTreasury; uint256 public fee; - // Metadata + // The contract metadata string public name; string public endpoint; - // Main calculation storage + // The main calculation storage uint256 public totalStaked; uint256 public totalRate; uint256 public lastContractBalance; + + // The Staker data bool public isStakeClosed; mapping(address => Staker) public stakers; // Deregistration limits + bool isDeregistered; uint128 public deregistrationOpenAt; - uint128 public deregistrationTimeout; - uint128 public deregistrationNonFeeOpened; - uint128 public deregistrationNonFeePeriod; constructor() { _disableInitializers(); } - function ProvidersDelegator_init( + function ProvidersDelegate_init( address lumerinDiamond_, address feeTreasury_, uint256 fee_, string memory name_, string memory endpoint_, - uint128 deregistrationTimeout_, - uint128 deregistrationNonFeePeriod_ + uint128 deregistrationOpenAt_ ) external initializer { __Ownable_init(); @@ -66,13 +65,11 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { if (fee_ > PRECISION) { revert InvalidFee(fee_, PRECISION); } + fee = fee_; + deregistrationOpenAt = deregistrationOpenAt_; IERC20(token).approve(lumerinDiamond_, type(uint256).max); - - deregistrationTimeout = deregistrationTimeout_; - deregistrationOpenAt = uint128(block.timestamp) + deregistrationTimeout_; - deregistrationNonFeePeriod = deregistrationNonFeePeriod_; } function setName(string memory name_) public onlyOwner { @@ -118,21 +115,26 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { } function stake(uint256 amount_) external { + _stake(_msgSender(), amount_); + } + + function _stake(address staker_, uint256 amount_) private { if (isStakeClosed) { revert StakeClosed(); } - + if (isDeregistered) { + revert ProviderDeregistered(); + } if (amount_ == 0) { revert InsufficientAmount(); } - address user_ = _msgSender(); - Staker storage staker = stakers[user_]; + Staker storage staker = stakers[staker_]; (uint256 currentRate_, uint256 contractBalance_) = getCurrentRate(); uint256 pendingRewards_ = _getCurrentStakerRewards(currentRate_, staker); - IERC20(token).safeTransferFrom(user_, address(this), amount_); + IERC20(token).safeTransferFrom(staker_, address(this), amount_); totalRate = currentRate_; totalStaked += amount_; @@ -145,51 +147,22 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { IProviderRegistry(lumerinDiamond).providerRegister(address(this), amount_, endpoint); - emit Staked(user_, staker.staked, staker.pendingRewards, staker.rate); - } + emit Staked(staker_, staker.staked, totalStaked, staker.rate); + } function restake(address staker_, uint256 amount_) external { if (_msgSender() != staker_ && _msgSender() != owner()) { revert RestakeInvalidCaller(_msgSender(), staker_); } - - Staker storage staker = stakers[staker_]; - if (staker.isRestakeDisabled) { + if (_msgSender() == owner() && stakers[staker_].isRestakeDisabled) { revert RestakeDisabled(staker_); } - (uint256 currentRate_, uint256 contractBalance_) = getCurrentRate(); - uint256 pendingRewards_ = _getCurrentStakerRewards(currentRate_, staker); - - amount_ = amount_.min(contractBalance_).min(pendingRewards_); - if (amount_ == 0) { - revert InsufficientAmount(); - } - - uint256 feeAmount_ = (amount_ * fee) / PRECISION; - uint256 amountWithFee_ = amount_ - feeAmount_; - if (feeAmount_ != 0) { - IERC20(token).safeTransfer(feeTreasury, feeAmount_); - - emit FeeClaimed(feeTreasury, feeAmount_); - } - - IProviderRegistry(lumerinDiamond).providerRegister(address(this), amountWithFee_, endpoint); - - totalRate = currentRate_; - totalStaked += amountWithFee_; - - lastContractBalance = contractBalance_ - amount_; - - staker.rate = currentRate_; - staker.staked += amountWithFee_; - staker.claimed += amount_; - staker.pendingRewards = pendingRewards_ - amount_; - - emit Restaked(staker_, staker.staked, staker.pendingRewards, staker.rate); + amount_ = claim(staker_, amount_); + _stake(staker_, amount_); } - function claim(address staker_, uint256 amount_) external { + function claim(address staker_, uint256 amount_) public returns (uint256) { Staker storage staker = stakers[staker_]; (uint256 currentRate_, uint256 contractBalance_) = getCurrentRate(); @@ -209,7 +182,7 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { staker.claimed += amount_; uint256 feeAmount_ = (amount_ * fee) / PRECISION; - if (feeAmount_ != 0 && block.timestamp > deregistrationNonFeeOpened + deregistrationNonFeePeriod) { + if (feeAmount_ != 0) { IERC20(token).safeTransfer(feeTreasury, feeAmount_); amount_ -= feeAmount_; @@ -219,7 +192,9 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { IERC20(token).safeTransfer(staker_, amount_); - emit Claimed(staker_, staker.staked, staker.pendingRewards, staker.rate); + emit Claimed(staker_, staker.claimed, staker.rate); + + return amount_; } function getCurrentRate() public view returns (uint256, uint256) { @@ -242,30 +217,40 @@ contract ProvidersDelegator is IProvidersDelegator, OwnableUpgradeable { } function providerDeregister(bytes32[] calldata bidIds_) external { - if (block.timestamp < deregistrationOpenAt) { + if (!isDeregisterAvailable()) { _checkOwner(); - } else { - deregistrationOpenAt = uint128(block.timestamp) + deregistrationTimeout; + } + if (isDeregistered) { + revert ProviderDeregistered(); } _deleteModelBids(bidIds_); IProviderRegistry(lumerinDiamond).providerDeregister(address(this)); - deregistrationNonFeeOpened = uint128(block.timestamp); + isDeregistered = true; + fee = 0; } function postModelBid(bytes32 modelId_, uint256 pricePerSecond_) external onlyOwner returns (bytes32) { + if (isDeregisterAvailable()) { + revert BidCannotBeCreatedDuringThisPeriod(); + } + return IMarketplace(lumerinDiamond).postModelBid(address(this), modelId_, pricePerSecond_); } function deleteModelBids(bytes32[] calldata bidIds_) external { - if (block.timestamp < deregistrationOpenAt) { + if (!isDeregisterAvailable()) { _checkOwner(); } _deleteModelBids(bidIds_); } + function isDeregisterAvailable() public view returns (bool) { + return block.timestamp >= deregistrationOpenAt; + } + function _deleteModelBids(bytes32[] calldata bidIds_) private { address lumerinDiamond_ = lumerinDiamond; diff --git a/smart-contracts/contracts/interfaces/delegate/IDelegatorFactory.sol b/smart-contracts/contracts/interfaces/delegate/IDelegateFactory.sol similarity index 69% rename from smart-contracts/contracts/interfaces/delegate/IDelegatorFactory.sol rename to smart-contracts/contracts/interfaces/delegate/IDelegateFactory.sol index 4f059c1f..c8c0f972 100644 --- a/smart-contracts/contracts/interfaces/delegate/IDelegatorFactory.sol +++ b/smart-contracts/contracts/interfaces/delegate/IDelegateFactory.sol @@ -1,19 +1,28 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -interface IDelegatorFactory { +interface IDelegateFactory { + error InvalidDeregistrationOpenAt(uint128 value, uint128 minimal); + /** * The event that is emitted when the proxy deployed. * @param proxyAddress The pool's id. */ event ProxyDeployed(address indexed proxyAddress); + /** + * The event that is emitted when the `minDeregistrationTimeout` changed. + * @param minDeregistrationTimeout_ The pool's id. + */ + event MinDeregistrationTimeoutUpdated(uint128 minDeregistrationTimeout_); + /** * The function to initialize the contract. * @param lumerinDiamond_ The Lumerin protocol address. * @param implementation_ The implementation address. + * @param minDeregistrationTimeout_ The minimal timestamp before deregistration will start */ - function DelegatorFactory_init(address lumerinDiamond_, address implementation_) external; + function DelegateFactory_init(address lumerinDiamond_, address implementation_, uint128 minDeregistrationTimeout_) external; /** * Triggers stopped state. @@ -31,8 +40,7 @@ interface IDelegatorFactory { * @param fee_ The fee percent where 100% = 10^25. * @param name_ The Subnet name. * @param endpoint_ The subnet endpoint. - * @param deregistrationTimeout_ Provider deregistration will be available after this timeout. - * @param deregistrationNonFeePeriod_ Period after deregistration when Stakers can claim rewards without fee. + * @param deregistrationOpenAt Provider deregistration will be available after this timestamp. * @return Deployed proxy address */ function deployProxy( @@ -40,8 +48,7 @@ interface IDelegatorFactory { uint256 fee_, string memory name_, string memory endpoint_, - uint128 deregistrationTimeout_, - uint128 deregistrationNonFeePeriod_ + uint128 deregistrationOpenAt ) external returns (address); /** diff --git a/smart-contracts/contracts/interfaces/delegate/IProvidersDelegator.sol b/smart-contracts/contracts/interfaces/delegate/IProvidersDelegate.sol similarity index 68% rename from smart-contracts/contracts/interfaces/delegate/IProvidersDelegator.sol rename to smart-contracts/contracts/interfaces/delegate/IProvidersDelegate.sol index 84bd7fe8..0e601818 100644 --- a/smart-contracts/contracts/interfaces/delegate/IProvidersDelegator.sol +++ b/smart-contracts/contracts/interfaces/delegate/IProvidersDelegate.sol @@ -1,27 +1,76 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -interface IProvidersDelegator { - event NameUpdated(string name); - event EndpointUpdated(string endpoint); - event FeeTreasuryUpdated(address feeTreasury); - event IsStakeClosedUpdated(bool isStakeClosed); - event IsRestakeDisabledUpdated(address staker, bool isRestakeDisabled); - event Staked(address staker, uint256 staked, uint256 pendingRewards, uint256 rate); - event Restaked(address staker, uint256 staked, uint256 pendingRewards, uint256 rate); - event Claimed(address staker, uint256 staked, uint256 pendingRewards, uint256 rate); - event FeeClaimed(address feeTreasury, uint256 feeAmount); - +interface IProvidersDelegate { error InvalidNameLength(); error InvalidEndpointLength(); error InvalidFeeTreasuryAddress(); error InvalidFee(uint256 current, uint256 max); error StakeClosed(); + error ProviderDeregistered(); + error BidCannotBeCreatedDuringThisPeriod(); error InsufficientAmount(); error RestakeDisabled(address staker); error RestakeInvalidCaller(address caller, address staker); error ClaimAmountIsZero(); + /** + * The event that is emitted when the name updated. + * @param name The new value. + */ + event NameUpdated(string name); + + /** + * The event that is emitted when the endpoint updated. + * @param endpoint The new value. + */ + event EndpointUpdated(string endpoint); + + /** + * The event that is emitted when the `feeTreasury` updated. + * @param feeTreasury The new value. + */ + event FeeTreasuryUpdated(address feeTreasury); + + /** + * The event that is emitted when the stake closed or opened for all users. + * @param isStakeClosed The new value. + */ + event IsStakeClosedUpdated(bool isStakeClosed); + + /** + * The event that is emitted when restake was disabled or enabled by user. + * @param staker The user address. + * @param isRestakeDisabled The new value. + */ + event IsRestakeDisabledUpdated(address staker, bool isRestakeDisabled); + + /** + * The event that is emitted when user staked. + * @param staker The user address. + * @param staked The total staked amount for user. + * @param totalStaked The total staked amount for the contract. + * @param rate The contract rate. + */ + event Staked(address staker, uint256 staked, uint256 totalStaked, uint256 rate); + + /** + * The event that is emitted when user claimed. + * @param staker The user address. + * @param claimed The total claimed amount for user. + * @param rate The contract rate. + */ + event Claimed(address staker, uint256 claimed, uint256 rate); + + /** + * The event that is emitted when rewards claimed for owner. + * @param feeTreasury The fee treasury address. + * @param feeAmount The fee amount. + */ + event FeeClaimed(address feeTreasury, uint256 feeAmount); + + + /** * @param staked Staked amount. * @param claimed Claimed amount. @@ -44,17 +93,15 @@ interface IProvidersDelegator { * @param fee_ The fee percent where 100% = 10^25. * @param name_ The Subnet name. * @param endpoint_ The subnet endpoint. - * @param deregistrationTimeout_ Provider deregistration will be available after this timeout. - * @param deregistrationNonFeePeriod_ Period after deregistration when Stakers can claim rewards without fee. + * @param deregistrationOpenAt Provider deregistration will be available after this time. */ - function ProvidersDelegator_init( + function ProvidersDelegate_init( address lumerinDiamond_, address feeTreasury_, uint256 fee_, string memory name_, string memory endpoint_, - uint128 deregistrationTimeout_, - uint128 deregistrationNonFeePeriod_ + uint128 deregistrationOpenAt ) external; /** @@ -105,7 +152,7 @@ interface IProvidersDelegator { * @param staker_ Staker address. * @param amount_ Amount to stake. */ - function claim(address staker_, uint256 amount_) external; + function claim(address staker_, uint256 amount_) external returns(uint256); /** * The function to return the current rate. diff --git a/smart-contracts/test/delegate/DelegatorFactory.test.ts b/smart-contracts/test/delegate/DelegateFactory.test.ts similarity index 64% rename from smart-contracts/test/delegate/DelegatorFactory.test.ts rename to smart-contracts/test/delegate/DelegateFactory.test.ts index 2715750d..5b6a447f 100644 --- a/smart-contracts/test/delegate/DelegatorFactory.test.ts +++ b/smart-contracts/test/delegate/DelegateFactory.test.ts @@ -1,9 +1,9 @@ import { - DelegatorFactory, + DelegateFactory, LumerinDiamond, MorpheusToken, - ProvidersDelegator, - ProvidersDelegator__factory, + ProvidersDelegate, + ProvidersDelegate__factory, UUPSMock, } from '@ethers-v6'; import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; @@ -12,7 +12,7 @@ import { ethers } from 'hardhat'; import { wei } from '@/scripts/utils/utils'; import { - deployDelegatorFactory, + deployDelegateFactory, deployFacetMarketplace, deployFacetProviderRegistry, deployLumerinDiamond, @@ -20,7 +20,7 @@ import { } from '@/test/helpers/deployers'; import { Reverter } from '@/test/helpers/reverter'; -describe('DelegatorFactory', () => { +describe('DelegateFactory', () => { const reverter = new Reverter(); let OWNER: SignerWithAddress; @@ -28,7 +28,7 @@ describe('DelegatorFactory', () => { let SHEV: SignerWithAddress; let diamond: LumerinDiamond; - let delegatorFactory: DelegatorFactory; + let delegatorFactory: DelegateFactory; let token: MorpheusToken; @@ -41,7 +41,7 @@ describe('DelegatorFactory', () => { deployFacetMarketplace(diamond, token, wei(0.0001), wei(900)), ]); - delegatorFactory = await deployDelegatorFactory(diamond); + delegatorFactory = await deployDelegateFactory(diamond, 3600); await reverter.snapshot(); }); @@ -49,9 +49,9 @@ describe('DelegatorFactory', () => { afterEach(reverter.revert); describe('UUPS', () => { - describe('#DelegatorFactory_init', () => { + describe('#DelegateFactory_init', () => { it('should revert if try to call init function twice', async () => { - await expect(delegatorFactory.DelegatorFactory_init(OWNER, OWNER)).to.be.rejectedWith( + await expect(delegatorFactory.DelegateFactory_init(OWNER, OWNER, 3600)).to.be.rejectedWith( 'Initializable: contract is already initialized', ); }); @@ -79,66 +79,90 @@ describe('DelegatorFactory', () => { }); }); + describe('#setMinDeregistrationTimeout', () => { + it('should set the provider name', async () => { + expect(await delegatorFactory.minDeregistrationTimeout()).eq(3600); + + await delegatorFactory.setMinDeregistrationTimeout(100); + + expect(await delegatorFactory.minDeregistrationTimeout()).eq(100); + }); + it('should throw error when name is zero', async () => { + await expect(delegatorFactory.connect(SHEV).setMinDeregistrationTimeout(100)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + describe('#deployProxy', () => { - let providersDelegatorFactory: ProvidersDelegator__factory; + let providersDelegateFactory: ProvidersDelegate__factory; before(async () => { - providersDelegatorFactory = await ethers.getContractFactory('ProvidersDelegator'); + providersDelegateFactory = await ethers.getContractFactory('ProvidersDelegate'); }); it('should deploy a new proxy', async () => { - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); - const proxy = providersDelegatorFactory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegator; + const proxy = providersDelegateFactory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegate; expect(await proxy.owner()).to.eq(SHEV); expect(await proxy.fee()).to.eq(wei(0.1, 25)); expect(await proxy.feeTreasury()).to.eq(KYLE); expect(await proxy.name()).to.eq('name'); expect(await proxy.endpoint()).to.eq('endpoint'); + expect(await proxy.deregistrationOpenAt()).to.eq(9998887771); }); it('should deploy new proxies', async () => { - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 1, 2); - await delegatorFactory.connect(SHEV).deployProxy(SHEV, wei(0.2, 25), 'name2', 'endpoint2', 1, 2); - await delegatorFactory.connect(KYLE).deployProxy(SHEV, wei(0.3, 25), 'name3', 'endpoint3', 1, 2); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 9998887771); + await delegatorFactory.connect(SHEV).deployProxy(SHEV, wei(0.2, 25), 'name2', 'endpoint2', 9998887772); + await delegatorFactory.connect(KYLE).deployProxy(SHEV, wei(0.3, 25), 'name3', 'endpoint3', 9998887773); - let proxy = providersDelegatorFactory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegator; + let proxy = providersDelegateFactory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegate; expect(await proxy.owner()).to.eq(SHEV); expect(await proxy.fee()).to.eq(wei(0.2, 25)); expect(await proxy.feeTreasury()).to.eq(SHEV); expect(await proxy.name()).to.eq('name2'); expect(await proxy.endpoint()).to.eq('endpoint2'); + expect(await proxy.deregistrationOpenAt()).to.eq(9998887772); - proxy = providersDelegatorFactory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegator; + proxy = providersDelegateFactory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegate; expect(await proxy.owner()).to.eq(KYLE); expect(await proxy.fee()).to.eq(wei(0.3, 25)); expect(await proxy.feeTreasury()).to.eq(SHEV); expect(await proxy.name()).to.eq('name3'); expect(await proxy.endpoint()).to.eq('endpoint3'); + expect(await proxy.deregistrationOpenAt()).to.eq(9998887773); }); - describe('#pause, #unpause', () => { - it('should revert when paused and not after the unpause', async () => { - await delegatorFactory.pause(); - await expect( - delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 1, 2), - ).to.be.rejectedWith('Pausable: paused'); + it('should throw error when fee is invalid', async () => { + await expect( + delegatorFactory.deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 1), + ).to.be.revertedWithCustomError(delegatorFactory, 'InvalidDeregistrationOpenAt'); + }); + }); - await delegatorFactory.unpause(); - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 1, 2); - }); - it('should throw error when caller is not an owner', async () => { - await expect(delegatorFactory.connect(KYLE).pause()).to.be.revertedWith('Ownable: caller is not the owner'); - }); - it('should throw error when caller is not an owner', async () => { - await expect(delegatorFactory.connect(KYLE).unpause()).to.be.revertedWith('Ownable: caller is not the owner'); - }); + describe('#pause, #unpause', () => { + it('should revert when paused and not after the unpause', async () => { + await delegatorFactory.pause(); + await expect( + delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 9998887771), + ).to.be.rejectedWith('Pausable: paused'); + + await delegatorFactory.unpause(); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 9998887771); + }); + it('should throw error when caller is not an owner', async () => { + await expect(delegatorFactory.connect(KYLE).pause()).to.be.revertedWith('Ownable: caller is not the owner'); + }); + it('should throw error when caller is not an owner', async () => { + await expect(delegatorFactory.connect(KYLE).unpause()).to.be.revertedWith('Ownable: caller is not the owner'); }); }); describe('#predictProxyAddress', () => { it('should predict a proxy address', async () => { const predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); const proxyAddress = await delegatorFactory.proxies(SHEV, 0); @@ -146,46 +170,46 @@ describe('DelegatorFactory', () => { }); it('should predict proxy addresses', async () => { let predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); expect(await delegatorFactory.proxies(SHEV, 0)).to.eq(predictedProxyAddress); predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); expect(await delegatorFactory.proxies(SHEV, 1)).to.eq(predictedProxyAddress); predictedProxyAddress = await delegatorFactory.predictProxyAddress(KYLE); - await delegatorFactory.connect(KYLE).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); + await delegatorFactory.connect(KYLE).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); expect(await delegatorFactory.proxies(KYLE, 0)).to.eq(predictedProxyAddress); predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); expect(await delegatorFactory.proxies(SHEV, 2)).to.eq(predictedProxyAddress); }); }); describe('#updateImplementation', () => { it('should update proxies implementation', async () => { - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); - await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); - await delegatorFactory.connect(KYLE).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 1, 2); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); + await delegatorFactory.connect(KYLE).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); const factory = await ethers.getContractFactory('UUPSMock'); const newImpl = await factory.deploy(); - let proxy = factory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegator; + let proxy = factory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegate; expect(await proxy.version()).to.eq(1); - proxy = factory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegator; + proxy = factory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegate; expect(await proxy.version()).to.eq(1); - proxy = factory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegator; + proxy = factory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegate; expect(await proxy.version()).to.eq(1); await delegatorFactory.updateImplementation(newImpl); - proxy = factory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegator; + proxy = factory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegate; expect(await proxy.version()).to.eq(999); - proxy = factory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegator; + proxy = factory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegate; expect(await proxy.version()).to.eq(999); - proxy = factory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegator; + proxy = factory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegate; expect(await proxy.version()).to.eq(999); }); it('should throw error when caller is not an owner', async () => { @@ -196,5 +220,5 @@ describe('DelegatorFactory', () => { }); }); -// npm run generate-types && npx hardhat test "test/delegate/DelegatorFactory.test" -// npx hardhat coverage --solcoverjs ./.solcover.ts --testfiles "test/delegate/DelegatorFactory.test" +// npm run generate-types && npx hardhat test "test/delegate/DelegateFactory.test.ts" +// npx hardhat coverage --solcoverjs ./.solcover.ts --testfiles "test/delegate/DelegateFactory.test.ts" diff --git a/smart-contracts/test/delegate/ProviderDelegator.test.ts b/smart-contracts/test/delegate/ProvidersDelegate.test.ts similarity index 59% rename from smart-contracts/test/delegate/ProviderDelegator.test.ts rename to smart-contracts/test/delegate/ProvidersDelegate.test.ts index 09630af4..5e077c52 100644 --- a/smart-contracts/test/delegate/ProviderDelegator.test.ts +++ b/smart-contracts/test/delegate/ProvidersDelegate.test.ts @@ -4,7 +4,7 @@ import { ModelRegistry, MorpheusToken, ProviderRegistry, - ProvidersDelegator, + ProvidersDelegate, SessionRouter, } from '@ethers-v6'; import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; @@ -25,14 +25,14 @@ import { deployFacetSessionRouter, deployLumerinDiamond, deployMORToken, - deployProvidersDelegator, + deployProvidersDelegate, } from '@/test/helpers/deployers'; import { Reverter } from '@/test/helpers/reverter'; import { setTime } from '@/utils/block-helper'; import { getProviderApproval, getReceipt } from '@/utils/provider-helper'; import { DAY } from '@/utils/time'; -describe('ProvidersDelegator', () => { +describe('ProvidersDelegate', () => { const reverter = new Reverter(); let OWNER: SignerWithAddress; @@ -45,7 +45,7 @@ describe('ProvidersDelegator', () => { let diamond: LumerinDiamond; let providerRegistry: ProviderRegistry; let modelRegistry: ModelRegistry; - let providersDelegator: ProvidersDelegator; + let providersDelegate: ProvidersDelegate; let marketplace: Marketplace; let sessionRouter: SessionRouter; @@ -53,7 +53,6 @@ describe('ProvidersDelegator', () => { let delegateRegistry: DelegateRegistry; before(async () => { - // await setTime(5000); [OWNER, DELEGATOR, TREASURY, KYLE, SHEV, ALAN] = await ethers.getSigners(); [diamond, token, delegateRegistry] = await Promise.all([ @@ -70,14 +69,13 @@ describe('ProvidersDelegator', () => { deployFacetDelegation(diamond, delegateRegistry), ]); - providersDelegator = await deployProvidersDelegator( + providersDelegate = await deployProvidersDelegate( diamond, await TREASURY.getAddress(), wei(0.2, 25), 'DLNAME', 'ENDPOINT', - 3600, - 300, + payoutStart + 3 * DAY, ); await token.transfer(KYLE, wei(1000)); @@ -87,9 +85,9 @@ describe('ProvidersDelegator', () => { await token.connect(OWNER).approve(sessionRouter, wei(1000)); await token.connect(ALAN).approve(sessionRouter, wei(1000)); - await token.connect(KYLE).approve(providersDelegator, wei(1000)); - await token.connect(SHEV).approve(providersDelegator, wei(1000)); - await token.connect(ALAN).approve(providersDelegator, wei(1000)); + await token.connect(KYLE).approve(providersDelegate, wei(1000)); + await token.connect(SHEV).approve(providersDelegate, wei(1000)); + await token.connect(ALAN).approve(providersDelegate, wei(1000)); await token.connect(DELEGATOR).approve(modelRegistry, wei(1000)); await reverter.snapshot(); @@ -97,50 +95,47 @@ describe('ProvidersDelegator', () => { afterEach(reverter.revert); - describe('#ProvidersDelegator_init', () => { + describe('#providersDelegate_init', () => { it('should revert if try to call init function twice', async () => { await expect( - providersDelegator.ProvidersDelegator_init(OWNER, await TREASURY.getAddress(), 1, '', '', 0, 0), + providersDelegate.ProvidersDelegate_init(OWNER, await TREASURY.getAddress(), 1, '', '', 0), ).to.be.rejectedWith('Initializable: contract is already initialized'); }); it('should throw error when fee is invalid', async () => { await expect( - deployProvidersDelegator(diamond, await TREASURY.getAddress(), wei(1.1, 25), 'DLNAME', 'ENDPOINT', 3600, 300), - ).to.be.revertedWithCustomError(providersDelegator, 'InvalidFee'); + deployProvidersDelegate(diamond, await TREASURY.getAddress(), wei(1.1, 25), 'DLNAME', 'ENDPOINT', 0n), + ).to.be.revertedWithCustomError(providersDelegate, 'InvalidFee'); }); }); describe('#setName', () => { it('should set the provider name', async () => { - await providersDelegator.setName('TEST'); + await providersDelegate.setName('TEST'); - expect(await providersDelegator.name()).eq('TEST'); + expect(await providersDelegate.name()).eq('TEST'); }); it('should throw error when name is zero', async () => { - await expect(providersDelegator.setName('')).to.be.revertedWithCustomError( - providersDelegator, - 'InvalidNameLength', - ); + await expect(providersDelegate.setName('')).to.be.revertedWithCustomError(providersDelegate, 'InvalidNameLength'); }); it('should throw error when caller is not an owner', async () => { - await expect(providersDelegator.connect(KYLE).setName('')).to.be.revertedWith('Ownable: caller is not the owner'); + await expect(providersDelegate.connect(KYLE).setName('')).to.be.revertedWith('Ownable: caller is not the owner'); }); }); describe('#setEndpoint', () => { it('should set the provider endpoint', async () => { - await providersDelegator.setEndpoint('TEST'); + await providersDelegate.setEndpoint('TEST'); - expect(await providersDelegator.endpoint()).eq('TEST'); + expect(await providersDelegate.endpoint()).eq('TEST'); }); it('should throw error when endpoint is zero', async () => { - await expect(providersDelegator.setEndpoint('')).to.be.revertedWithCustomError( - providersDelegator, + await expect(providersDelegate.setEndpoint('')).to.be.revertedWithCustomError( + providersDelegate, 'InvalidEndpointLength', ); }); it('should throw error when caller is not an owner', async () => { - await expect(providersDelegator.connect(KYLE).setEndpoint('')).to.be.revertedWith( + await expect(providersDelegate.connect(KYLE).setEndpoint('')).to.be.revertedWith( 'Ownable: caller is not the owner', ); }); @@ -148,18 +143,18 @@ describe('ProvidersDelegator', () => { describe('#setFeeTreasuryTreasury', () => { it('should set the provider fee', async () => { - await providersDelegator.setFeeTreasury(KYLE); + await providersDelegate.setFeeTreasury(KYLE); - expect(await providersDelegator.feeTreasury()).eq(KYLE); + expect(await providersDelegate.feeTreasury()).eq(KYLE); }); it('should throw error when fee treasury is invalid', async () => { - await expect(providersDelegator.setFeeTreasury(ZERO_ADDR)).to.be.revertedWithCustomError( - providersDelegator, + await expect(providersDelegate.setFeeTreasury(ZERO_ADDR)).to.be.revertedWithCustomError( + providersDelegate, 'InvalidFeeTreasuryAddress', ); }); it('should throw error when caller is not an owner', async () => { - await expect(providersDelegator.connect(KYLE).setFeeTreasury(KYLE)).to.be.revertedWith( + await expect(providersDelegate.connect(KYLE).setFeeTreasury(KYLE)).to.be.revertedWith( 'Ownable: caller is not the owner', ); }); @@ -167,12 +162,12 @@ describe('ProvidersDelegator', () => { describe('#setIsStakeClosed', () => { it('should set the isStakeClosed flag', async () => { - await providersDelegator.setIsStakeClosed(true); + await providersDelegate.setIsStakeClosed(true); - expect(await providersDelegator.isStakeClosed()).eq(true); + expect(await providersDelegate.isStakeClosed()).eq(true); }); it('should throw error when caller is not an owner', async () => { - await expect(providersDelegator.connect(KYLE).setIsStakeClosed(true)).to.be.revertedWith( + await expect(providersDelegate.connect(KYLE).setIsStakeClosed(true)).to.be.revertedWith( 'Ownable: caller is not the owner', ); }); @@ -180,53 +175,62 @@ describe('ProvidersDelegator', () => { describe('#stake', () => { it('should stake tokens, one staker', async () => { - await providersDelegator.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(KYLE).stake(wei(100)); - const staker = await providersDelegator.stakers(KYLE); + const staker = await providersDelegate.stakers(KYLE); expect(staker.staked).to.eq(wei(100)); expect(staker.pendingRewards).to.eq(wei(0)); expect(staker.isRestakeDisabled).to.eq(false); - expect(await providersDelegator.totalStaked()).to.eq(wei(100)); + expect(await providersDelegate.totalStaked()).to.eq(wei(100)); - expect(await token.balanceOf(providersDelegator)).to.eq(wei(0)); + expect(await token.balanceOf(providersDelegate)).to.eq(wei(0)); expect(await token.balanceOf(providerRegistry)).to.eq(wei(100)); expect(await token.balanceOf(KYLE)).to.eq(wei(900)); }); it('should stake tokens, two staker', async () => { - await providersDelegator.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(KYLE).stake(wei(100)); - const staker1 = await providersDelegator.stakers(KYLE); + const staker1 = await providersDelegate.stakers(KYLE); expect(staker1.staked).to.eq(wei(100)); expect(staker1.pendingRewards).to.eq(wei(0)); expect(staker1.isRestakeDisabled).to.eq(false); - expect(await providersDelegator.totalStaked()).to.eq(wei(100)); + expect(await providersDelegate.totalStaked()).to.eq(wei(100)); - await providersDelegator.connect(SHEV).stake(wei(200)); + await providersDelegate.connect(SHEV).stake(wei(200)); - const staker2 = await providersDelegator.stakers(SHEV); + const staker2 = await providersDelegate.stakers(SHEV); expect(staker2.staked).to.eq(wei(200)); expect(staker2.pendingRewards).to.eq(wei(0)); expect(staker2.isRestakeDisabled).to.eq(false); - expect(await providersDelegator.totalStaked()).to.eq(wei(300)); + expect(await providersDelegate.totalStaked()).to.eq(wei(300)); - expect(await token.balanceOf(providersDelegator)).to.eq(wei(0)); + expect(await token.balanceOf(providersDelegate)).to.eq(wei(0)); expect(await token.balanceOf(providerRegistry)).to.eq(wei(300)); expect(await token.balanceOf(KYLE)).to.eq(wei(900)); expect(await token.balanceOf(SHEV)).to.eq(wei(800)); }); it('should throw error when the stake is too low', async () => { - await expect(providersDelegator.connect(KYLE).stake(wei(0))).to.be.revertedWithCustomError( - providersDelegator, + await expect(providersDelegate.connect(KYLE).stake(wei(0))).to.be.revertedWithCustomError( + providersDelegate, 'InsufficientAmount', ); }); it('should throw error when the stake closed', async () => { - await providersDelegator.setIsStakeClosed(true); - await expect(providersDelegator.connect(KYLE).stake(wei(1))).to.be.revertedWithCustomError( - providersDelegator, + await providersDelegate.setIsStakeClosed(true); + await expect(providersDelegate.connect(KYLE).stake(wei(1))).to.be.revertedWithCustomError( + providersDelegate, 'StakeClosed', ); }); + it('should throw error when provider deregistered', async () => { + await providersDelegate.connect(KYLE).stake(wei(1)); + await providersDelegate.providerDeregister([]); + + await expect(providersDelegate.connect(KYLE).stake(wei(1))).to.be.revertedWithCustomError( + providersDelegate, + 'ProviderDeregistered', + ); + }); }); describe('#claim', () => { @@ -234,81 +238,81 @@ describe('ProvidersDelegator', () => { await setTime(5000); }); it('should correctly claim, one staker, full claim', async () => { - await providersDelegator.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(KYLE).stake(wei(100)); - await token.transfer(providersDelegator, wei(10)); + await token.transfer(providersDelegate, wei(10)); - expect(await providersDelegator.getCurrentStakerRewards(KYLE)).to.eq(wei(10)); + expect(await providersDelegate.getCurrentStakerRewards(KYLE)).to.eq(wei(10)); - await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + await providersDelegate.connect(KYLE).claim(KYLE, wei(9999)); expect(await token.balanceOf(KYLE)).to.eq(wei(908)); expect(await token.balanceOf(TREASURY)).to.eq(wei(2)); }); it('should correctly claim, one staker, partial claim', async () => { - await providersDelegator.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(KYLE).stake(wei(100)); - await token.transfer(providersDelegator, wei(20)); + await token.transfer(providersDelegate, wei(20)); - await providersDelegator.connect(KYLE).claim(KYLE, wei(5)); + await providersDelegate.connect(KYLE).claim(KYLE, wei(5)); expect(await token.balanceOf(KYLE)).to.eq(wei(904)); expect(await token.balanceOf(TREASURY)).to.eq(wei(1)); - await providersDelegator.connect(KYLE).claim(KYLE, wei(10)); + await providersDelegate.connect(KYLE).claim(KYLE, wei(10)); expect(await token.balanceOf(KYLE)).to.eq(wei(912)); expect(await token.balanceOf(TREASURY)).to.eq(wei(3)); - await providersDelegator.connect(KYLE).claim(KYLE, wei(5)); + await providersDelegate.connect(KYLE).claim(KYLE, wei(5)); expect(await token.balanceOf(KYLE)).to.eq(wei(916)); expect(await token.balanceOf(TREASURY)).to.eq(wei(4)); }); it('should correctly claim, two stakers, full claim, enter when no rewards distributed', async () => { - await providersDelegator.connect(KYLE).stake(wei(100)); - await providersDelegator.connect(SHEV).stake(wei(300)); + await providersDelegate.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(SHEV).stake(wei(300)); - await token.transfer(providersDelegator, wei(40)); + await token.transfer(providersDelegate, wei(40)); - await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + await providersDelegate.connect(KYLE).claim(KYLE, wei(9999)); expect(await token.balanceOf(KYLE)).to.eq(wei(908)); expect(await token.balanceOf(TREASURY)).to.eq(wei(2)); - await providersDelegator.connect(SHEV).claim(SHEV, wei(9999)); + await providersDelegate.connect(SHEV).claim(SHEV, wei(9999)); expect(await token.balanceOf(SHEV)).to.eq(wei(724)); expect(await token.balanceOf(TREASURY)).to.eq(wei(2 + 6)); }); it('should correctly claim, two stakers, partial claim, enter when rewards distributed', async () => { - await providersDelegator.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(KYLE).stake(wei(100)); - await token.transfer(providersDelegator, wei(10)); + await token.transfer(providersDelegate, wei(10)); - await providersDelegator.connect(SHEV).stake(wei(300)); + await providersDelegate.connect(SHEV).stake(wei(300)); - await token.transfer(providersDelegator, wei(40)); + await token.transfer(providersDelegate, wei(40)); - await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + await providersDelegate.connect(KYLE).claim(KYLE, wei(9999)); expect(await token.balanceOf(KYLE)).to.eq(wei(916)); // 10 + 25% from 40 expect(await token.balanceOf(TREASURY)).to.eq(wei(4)); - await providersDelegator.connect(SHEV).claim(SHEV, wei(20)); + await providersDelegate.connect(SHEV).claim(SHEV, wei(20)); expect(await token.balanceOf(SHEV)).to.eq(wei(716)); expect(await token.balanceOf(TREASURY)).to.eq(wei(4 + 4)); - await token.transfer(providersDelegator, wei(100)); + await token.transfer(providersDelegate, wei(100)); - await providersDelegator.connect(SHEV).claim(SHEV, wei(20)); + await providersDelegate.connect(SHEV).claim(SHEV, wei(20)); expect(await token.balanceOf(SHEV)).to.eq(wei(732)); expect(await token.balanceOf(TREASURY)).to.eq(wei(4 + 4 + 4)); - await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + await providersDelegate.connect(KYLE).claim(KYLE, wei(9999)); expect(await token.balanceOf(KYLE)).to.eq(wei(936)); expect(await token.balanceOf(TREASURY)).to.eq(wei(4 + 4 + 4 + 5)); - await providersDelegator.connect(SHEV).claim(SHEV, wei(999)); + await providersDelegate.connect(SHEV).claim(SHEV, wei(999)); expect(await token.balanceOf(SHEV)).to.eq(wei(784)); expect(await token.balanceOf(TREASURY)).to.eq(wei(4 + 4 + 4 + 5 + 13)); }); it('should throw error when nothing to claim', async () => { - await expect(providersDelegator.connect(KYLE).claim(KYLE, wei(999))).to.be.revertedWithCustomError( - providersDelegator, + await expect(providersDelegate.connect(KYLE).claim(KYLE, wei(999))).to.be.revertedWithCustomError( + providersDelegate, 'ClaimAmountIsZero', ); }); @@ -319,44 +323,44 @@ describe('ProvidersDelegator', () => { await setTime(5000); }); it('should correctly restake, two stakers, full restake', async () => { - await providersDelegator.connect(KYLE).stake(wei(100)); - await providersDelegator.connect(SHEV).stake(wei(300)); + await providersDelegate.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(SHEV).stake(wei(300)); - await token.transfer(providersDelegator, wei(100)); + await token.transfer(providersDelegate, wei(100)); - await providersDelegator.connect(OWNER).restake(KYLE, wei(9999)); - expect((await providersDelegator.stakers(KYLE)).staked).to.eq(wei(120)); + await providersDelegate.connect(OWNER).restake(KYLE, wei(9999)); + expect((await providersDelegate.stakers(KYLE)).staked).to.eq(wei(120)); expect(await token.balanceOf(KYLE)).to.eq(wei(900)); expect(await token.balanceOf(TREASURY)).to.eq(wei(5)); - await token.transfer(providersDelegator, wei(100)); + await token.transfer(providersDelegate, wei(100)); - await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + await providersDelegate.connect(KYLE).claim(KYLE, wei(9999)); expect(await token.balanceOf(KYLE)).to.closeTo(wei(900 + 28.57 * 0.8), wei(0.01)); expect(await token.balanceOf(TREASURY)).to.closeTo(wei(5 + 28.57 * 0.2), wei(0.01)); - await providersDelegator.connect(SHEV).claim(SHEV, wei(9999)); + await providersDelegate.connect(SHEV).claim(SHEV, wei(9999)); expect(await token.balanceOf(SHEV)).to.closeTo(wei(700 + 75 * 0.8 + 71.42 * 0.8), wei(0.01)); expect(await token.balanceOf(TREASURY)).to.closeTo(wei(5 + 28.57 * 0.2 + 75 * 0.2 + 71.42 * 0.2), wei(0.01)); }); it('should correctly restake, two stakers, partial restake', async () => { - await providersDelegator.connect(KYLE).stake(wei(100)); - await providersDelegator.connect(SHEV).stake(wei(300)); + await providersDelegate.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(SHEV).stake(wei(300)); - await token.transfer(providersDelegator, wei(100)); + await token.transfer(providersDelegate, wei(100)); - await providersDelegator.connect(OWNER).restake(KYLE, wei(20)); - expect((await providersDelegator.stakers(KYLE)).staked).to.eq(wei(116)); + await providersDelegate.connect(OWNER).restake(KYLE, wei(20)); + expect((await providersDelegate.stakers(KYLE)).staked).to.eq(wei(116)); expect(await token.balanceOf(KYLE)).to.eq(wei(900)); expect(await token.balanceOf(TREASURY)).to.eq(wei(4)); - await token.transfer(providersDelegator, wei(100)); + await token.transfer(providersDelegate, wei(100)); - await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + await providersDelegate.connect(KYLE).claim(KYLE, wei(9999)); expect(await token.balanceOf(KYLE)).to.closeTo(wei(900 + 5 * 0.8 + 27.88 * 0.8), wei(0.01)); expect(await token.balanceOf(TREASURY)).to.closeTo(wei(4 + 5 * 0.2 + 27.88 * 0.2), wei(0.01)); - await providersDelegator.connect(SHEV).claim(SHEV, wei(9999)); + await providersDelegate.connect(SHEV).claim(SHEV, wei(9999)); expect(await token.balanceOf(SHEV)).to.closeTo(wei(700 + 75 * 0.8 + 72.11 * 0.8), wei(0.01)); expect(await token.balanceOf(TREASURY)).to.closeTo( wei(4 + 5 * 0.2 + 27.88 * 0.2 + 75 * 0.2 + 72.11 * 0.2), @@ -364,47 +368,50 @@ describe('ProvidersDelegator', () => { ); }); it('should correctly restake with zero fee', async () => { - await providersDelegator.connect(KYLE).stake(wei(100)); - await token.transfer(providersDelegator, wei(10)); - await providersDelegator.connect(OWNER).restake(KYLE, 1); + await providersDelegate.connect(KYLE).stake(wei(100)); + await token.transfer(providersDelegate, wei(10)); + await providersDelegate.connect(OWNER).restake(KYLE, 1); expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); }); it('should throw error when restake caller is invalid', async () => { - await expect(providersDelegator.connect(KYLE).restake(SHEV, wei(999))).to.be.revertedWithCustomError( - providersDelegator, + await expect(providersDelegate.connect(KYLE).restake(SHEV, wei(999))).to.be.revertedWithCustomError( + providersDelegate, 'RestakeInvalidCaller', ); }); - it('should throw error when restake caller is invalid', async () => { - await providersDelegator.connect(SHEV).setIsRestakeDisabled(true); - await expect(providersDelegator.restake(SHEV, wei(999))).to.be.revertedWithCustomError( - providersDelegator, + it('should throw error when restake is disabled', async () => { + await providersDelegate.connect(SHEV).setIsRestakeDisabled(true); + await expect(providersDelegate.restake(SHEV, wei(999))).to.be.revertedWithCustomError( + providersDelegate, 'RestakeDisabled', ); }); - it('should throw error when restake amount is zero', async () => { - await expect(providersDelegator.restake(SHEV, wei(0))).to.be.revertedWithCustomError( - providersDelegator, - 'InsufficientAmount', - ); - }); }); describe('#providerDeregister', () => { it('should deregister the provider', async () => { - await providersDelegator.connect(KYLE).stake(wei(100)); - await providersDelegator.providerDeregister([]); + await providersDelegate.connect(KYLE).stake(wei(100)); + await providersDelegate.providerDeregister([]); - await providersDelegator.connect(KYLE).claim(KYLE, wei(9999)); + await providersDelegate.connect(KYLE).claim(KYLE, wei(9999)); expect(await token.balanceOf(KYLE)).to.eq(wei(1000)); expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); }); it('should throw error when caller is not an owner', async () => { - await expect(providersDelegator.connect(KYLE).providerDeregister([])).to.be.revertedWith( + await expect(providersDelegate.connect(KYLE).providerDeregister([])).to.be.revertedWith( 'Ownable: caller is not the owner', ); }); + it('should throw error when provider already deregistered', async () => { + await providersDelegate.connect(KYLE).stake(wei(100)); + await providersDelegate.providerDeregister([]); + + await expect(providersDelegate.providerDeregister([])).to.be.revertedWithCustomError( + providersDelegate, + 'ProviderDeregistered', + ); + }); }); describe('#postModelBid, #deleteModelBids', () => { @@ -412,7 +419,7 @@ describe('ProvidersDelegator', () => { it('should deregister the model bid and delete it', async () => { // Register provider - await providersDelegator.connect(SHEV).stake(wei(300)); + await providersDelegate.connect(SHEV).stake(wei(300)); // Register model await modelRegistry @@ -423,33 +430,41 @@ describe('ProvidersDelegator', () => { const modelId = await modelRegistry.getModelId(DELEGATOR, baseModelId); // Register bid - await providersDelegator.postModelBid(modelId, wei(0.0001)); - let bidId = await marketplace.getBidId(await providersDelegator.getAddress(), modelId, 0); + await providersDelegate.postModelBid(modelId, wei(0.0001)); + let bidId = await marketplace.getBidId(await providersDelegate.getAddress(), modelId, 0); - await providersDelegator.deleteModelBids([bidId]); + await providersDelegate.deleteModelBids([bidId]); // Register bid again and deregister not from OWNER - await providersDelegator.postModelBid(modelId, wei(0.0001)); - bidId = await marketplace.getBidId(await providersDelegator.getAddress(), modelId, 1); + await providersDelegate.postModelBid(modelId, wei(0.0001)); + bidId = await marketplace.getBidId(await providersDelegate.getAddress(), modelId, 1); - await setTime(10000); - await providersDelegator.connect(ALAN).deleteModelBids([bidId]); + await setTime(payoutStart + 10 * DAY); + await providersDelegate.connect(ALAN).deleteModelBids([bidId]); }); - it('should throw error when caller is not an owner', async () => { - await expect(providersDelegator.connect(KYLE).postModelBid(baseModelId, wei(0.0001))).to.be.revertedWith( + it('should throw error when caller is not an owner for `postModelBid`', async () => { + await expect(providersDelegate.connect(KYLE).postModelBid(baseModelId, wei(0.0001))).to.be.revertedWith( 'Ownable: caller is not the owner', ); }); - it('should throw error when caller is not an owner', async () => { - await expect(providersDelegator.connect(KYLE).deleteModelBids([baseModelId])).to.be.revertedWith( + it('should throw error when caller is not an owner for `deleteModelBids`', async () => { + await expect(providersDelegate.connect(KYLE).deleteModelBids([baseModelId])).to.be.revertedWith( 'Ownable: caller is not the owner', ); }); + it('should throw error when try to add bid after the deregistration opened', async () => { + await setTime(payoutStart + 10 * DAY); + + await expect(providersDelegate.postModelBid(baseModelId, wei(0.0001))).to.be.revertedWithCustomError( + providersDelegate, + 'BidCannotBeCreatedDuringThisPeriod', + ); + }); }); describe('#version', () => { it('should return the correct contract version', async () => { - expect(await providersDelegator.version()).to.eq(1); + expect(await providersDelegate.version()).to.eq(1); }); }); @@ -458,8 +473,8 @@ describe('ProvidersDelegator', () => { it('should claim correct reward amount', async () => { // Register provider - await providersDelegator.connect(KYLE).stake(wei(100)); - await providersDelegator.connect(SHEV).stake(wei(300)); + await providersDelegate.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(SHEV).stake(wei(300)); // Register model await modelRegistry @@ -470,13 +485,13 @@ describe('ProvidersDelegator', () => { const modelId = await modelRegistry.getModelId(DELEGATOR, baseModelId); // Register bid - await providersDelegator.postModelBid(modelId, wei(0.0001)); - const bidId = await marketplace.getBidId(await providersDelegator.getAddress(), modelId, 0); + await providersDelegate.postModelBid(modelId, wei(0.0001)); + const bidId = await marketplace.getBidId(await providersDelegate.getAddress(), modelId, 0); await setTime(payoutStart + 10 * DAY); const { msg, signature } = await getProviderApproval(OWNER, ALAN, bidId); await sessionRouter.connect(ALAN).openSession(ALAN, wei(50), false, msg, signature); - const sessionId = await sessionRouter.getSessionId(ALAN, providersDelegator, bidId, 0); + const sessionId = await sessionRouter.getSessionId(ALAN, providersDelegate, bidId, 0); const sessionTreasuryBalanceBefore = await token.balanceOf(OWNER); @@ -488,8 +503,8 @@ describe('ProvidersDelegator', () => { const sessionTreasuryBalanceAfter = await token.balanceOf(OWNER); const reward = sessionTreasuryBalanceBefore - sessionTreasuryBalanceAfter; - await providersDelegator.claim(KYLE, wei(9999)); - await providersDelegator.claim(SHEV, wei(9999)); + await providersDelegate.claim(KYLE, wei(9999)); + await providersDelegate.claim(SHEV, wei(9999)); expect(await token.balanceOf(KYLE)).to.eq(wei(900) + BigInt(Number(reward.toString()) * 0.25 * 0.8)); expect(await token.balanceOf(SHEV)).to.eq(wei(700) + BigInt(Number(reward.toString()) * 0.75 * 0.8)); expect(await token.balanceOf(TREASURY)).to.eq(BigInt(Number(reward.toString()) * 0.2)); @@ -499,8 +514,8 @@ describe('ProvidersDelegator', () => { await setTime(payoutStart + 1 * DAY); // Register provider - await providersDelegator.connect(KYLE).stake(wei(100)); - await providersDelegator.connect(SHEV).stake(wei(300)); + await providersDelegate.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(SHEV).stake(wei(300)); // Register model await modelRegistry @@ -511,14 +526,14 @@ describe('ProvidersDelegator', () => { const modelId = await modelRegistry.getModelId(DELEGATOR, baseModelId); // Register bid - await providersDelegator.postModelBid(modelId, wei(0.0001)); - const bidId = await marketplace.getBidId(await providersDelegator.getAddress(), modelId, 0); + await providersDelegate.postModelBid(modelId, wei(0.0001)); + const bidId = await marketplace.getBidId(await providersDelegate.getAddress(), modelId, 0); // Open session await setTime(payoutStart + 10 * DAY); const { msg, signature } = await getProviderApproval(OWNER, ALAN, bidId); await sessionRouter.connect(ALAN).openSession(ALAN, wei(50), false, msg, signature); - const sessionId = await sessionRouter.getSessionId(ALAN, providersDelegator, bidId, 0); + const sessionId = await sessionRouter.getSessionId(ALAN, providersDelegate, bidId, 0); // Close session await setTime(payoutStart + 15 * DAY); @@ -527,15 +542,15 @@ describe('ProvidersDelegator', () => { await sessionRouter.connect(ALAN).closeSession(receiptMsg, receiptSig); // Add the new Staker - await providersDelegator.connect(ALAN).stake(wei(1000)); + await providersDelegate.connect(ALAN).stake(wei(1000)); // Deregister the providers - await providersDelegator.connect(KYLE).providerDeregister([bidId]); + await providersDelegate.connect(KYLE).providerDeregister([bidId]); // Claim rewards - await providersDelegator.claim(KYLE, wei(9999)); - await providersDelegator.claim(SHEV, wei(9999)); - await providersDelegator.claim(ALAN, wei(9999)); + await providersDelegate.claim(KYLE, wei(9999)); + await providersDelegate.claim(SHEV, wei(9999)); + await providersDelegate.claim(ALAN, wei(9999)); expect(await token.balanceOf(KYLE)).to.closeTo(wei(1000), wei(0.1)); expect(await token.balanceOf(SHEV)).to.closeTo(wei(1000), wei(0.1)); expect(await token.balanceOf(ALAN)).to.closeTo(wei(1000), wei(0.2)); @@ -544,5 +559,5 @@ describe('ProvidersDelegator', () => { }); }); -// npm run generate-types && npx hardhat test "test/delegate/ProviderDelegator.test.ts" -// npx hardhat coverage --solcoverjs ./.solcover.ts --testfiles "test/delegate/ProviderDelegator.test.ts" +// npm run generate-types && npx hardhat test "test/delegate/ProvidersDelegate.test.ts" +// npx hardhat coverage --solcoverjs ./.solcover.ts --testfiles "test/delegate/ProvidersDelegate.test.ts" diff --git a/smart-contracts/test/helpers/deployers/delegate/delegate-factory.ts b/smart-contracts/test/helpers/deployers/delegate/delegate-factory.ts new file mode 100644 index 00000000..a185c522 --- /dev/null +++ b/smart-contracts/test/helpers/deployers/delegate/delegate-factory.ts @@ -0,0 +1,23 @@ +import { ethers } from 'hardhat'; + +import { DelegateFactory, LumerinDiamond } from '@/generated-types/ethers'; + +export const deployDelegateFactory = async ( + diamond: LumerinDiamond, + minDeregistrationTimeout: number, +): Promise => { + const [providersDelegateImplFactory, delegateFactoryImplFactory, proxyFactory] = await Promise.all([ + ethers.getContractFactory('ProvidersDelegate'), + ethers.getContractFactory('DelegateFactory'), + ethers.getContractFactory('ERC1967Proxy'), + ]); + + const delegatorFactoryImpl = await delegateFactoryImplFactory.deploy(); + const proxy = await proxyFactory.deploy(delegatorFactoryImpl, '0x'); + const delegatorFactory = delegatorFactoryImpl.attach(proxy) as DelegateFactory; + + const providersDelegateImpl = await providersDelegateImplFactory.deploy(); + await delegatorFactory.DelegateFactory_init(diamond, providersDelegateImpl, minDeregistrationTimeout); + + return delegatorFactory; +}; diff --git a/smart-contracts/test/helpers/deployers/delegate/delegator-factory.ts b/smart-contracts/test/helpers/deployers/delegate/delegator-factory.ts deleted file mode 100644 index dc4c8a6e..00000000 --- a/smart-contracts/test/helpers/deployers/delegate/delegator-factory.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ethers } from 'hardhat'; - -import { DelegatorFactory, LumerinDiamond } from '@/generated-types/ethers'; - -export const deployDelegatorFactory = async (diamond: LumerinDiamond): Promise => { - const [providersDelegatorImplFactory, delegatorFactoryImplFactory, proxyFactory] = await Promise.all([ - ethers.getContractFactory('ProvidersDelegator'), - ethers.getContractFactory('DelegatorFactory'), - ethers.getContractFactory('ERC1967Proxy'), - ]); - - const delegatorFactoryImpl = await delegatorFactoryImplFactory.deploy(); - const proxy = await proxyFactory.deploy(delegatorFactoryImpl, '0x'); - const delegatorFactory = delegatorFactoryImpl.attach(proxy) as DelegatorFactory; - - const providersDelegatorImpl = await providersDelegatorImplFactory.deploy(); - await delegatorFactory.DelegatorFactory_init(diamond, providersDelegatorImpl); - - return delegatorFactory; -}; diff --git a/smart-contracts/test/helpers/deployers/delegate/index.ts b/smart-contracts/test/helpers/deployers/delegate/index.ts index 462ee559..bcf0e085 100644 --- a/smart-contracts/test/helpers/deployers/delegate/index.ts +++ b/smart-contracts/test/helpers/deployers/delegate/index.ts @@ -1,2 +1,2 @@ -export * from './delegator-factory'; -export * from './providers-delegator'; +export * from './delegate-factory'; +export * from './providers-delegate'; diff --git a/smart-contracts/test/helpers/deployers/delegate/providers-delegator.ts b/smart-contracts/test/helpers/deployers/delegate/providers-delegate.ts similarity index 51% rename from smart-contracts/test/helpers/deployers/delegate/providers-delegator.ts rename to smart-contracts/test/helpers/deployers/delegate/providers-delegate.ts index 41d64de5..56c36f00 100644 --- a/smart-contracts/test/helpers/deployers/delegate/providers-delegator.ts +++ b/smart-contracts/test/helpers/deployers/delegate/providers-delegate.ts @@ -1,35 +1,26 @@ import { BigNumberish } from 'ethers'; import { ethers } from 'hardhat'; -import { LumerinDiamond, ProvidersDelegator } from '@/generated-types/ethers'; +import { LumerinDiamond, ProvidersDelegate } from '@/generated-types/ethers'; -export const deployProvidersDelegator = async ( +export const deployProvidersDelegate = async ( diamond: LumerinDiamond, feeTreasury: string, fee: BigNumberish, name: string, endpoint: string, - deregistrationTimeout: number, - deregistrationNonFeePeriod: number, -): Promise => { + deregistrationOpenAt_: bigint | number, +): Promise => { const [implFactory, proxyFactory] = await Promise.all([ - ethers.getContractFactory('ProvidersDelegator'), + ethers.getContractFactory('ProvidersDelegate'), ethers.getContractFactory('ERC1967Proxy'), ]); const impl = await implFactory.deploy(); const proxy = await proxyFactory.deploy(impl, '0x'); - const contract = implFactory.attach(proxy) as ProvidersDelegator; + const contract = implFactory.attach(proxy) as ProvidersDelegate; - await contract.ProvidersDelegator_init( - diamond, - feeTreasury, - fee, - name, - endpoint, - deregistrationTimeout, - deregistrationNonFeePeriod, - ); + await contract.ProvidersDelegate_init(diamond, feeTreasury, fee, name, endpoint, deregistrationOpenAt_); return contract; }; From 79558d8056610cb3354c40b53718b4e700875201 Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Thu, 19 Dec 2024 14:00:26 +0200 Subject: [PATCH 09/14] review fixes --- smart-contracts/contracts/delegate/DelegateFactory.sol | 8 ++++---- smart-contracts/contracts/delegate/ProvidersDelegate.sol | 8 ++++---- .../contracts/interfaces/delegate/IDelegateFactory.sol | 4 ++-- .../contracts/interfaces/delegate/IProvidersDelegate.sol | 4 ++-- smart-contracts/test/delegate/DelegateFactory.test.ts | 6 +++--- .../test/helpers/deployers/delegate/providers-delegate.ts | 4 ++-- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/smart-contracts/contracts/delegate/DelegateFactory.sol b/smart-contracts/contracts/delegate/DelegateFactory.sol index 475eeb05..8363b306 100644 --- a/smart-contracts/contracts/delegate/DelegateFactory.sol +++ b/smart-contracts/contracts/delegate/DelegateFactory.sol @@ -57,10 +57,10 @@ contract DelegateFactory is IDelegateFactory, OwnableUpgradeable, PausableUpgrad uint256 fee_, string memory name_, string memory endpoint_, - uint128 deregistrationOpenAt + uint128 deregistrationOpensAt_ ) external whenNotPaused returns (address) { - if (deregistrationOpenAt <= block.timestamp + minDeregistrationTimeout) { - revert InvalidDeregistrationOpenAt(deregistrationOpenAt, uint128(block.timestamp + minDeregistrationTimeout)); + if (deregistrationOpensAt_ <= block.timestamp + minDeregistrationTimeout) { + revert InvalidDeregistrationOpenAt(deregistrationOpensAt_, uint128(block.timestamp + minDeregistrationTimeout)); } bytes32 salt_ = _calculatePoolSalt(_msgSender()); @@ -74,7 +74,7 @@ contract DelegateFactory is IDelegateFactory, OwnableUpgradeable, PausableUpgrad fee_, name_, endpoint_, - deregistrationOpenAt + deregistrationOpensAt_ ); IOwnable(proxy_).transferOwnership(_msgSender()); diff --git a/smart-contracts/contracts/delegate/ProvidersDelegate.sol b/smart-contracts/contracts/delegate/ProvidersDelegate.sol index 84d6fb86..390d7e8a 100644 --- a/smart-contracts/contracts/delegate/ProvidersDelegate.sol +++ b/smart-contracts/contracts/delegate/ProvidersDelegate.sol @@ -39,7 +39,7 @@ contract ProvidersDelegate is IProvidersDelegate, OwnableUpgradeable { // Deregistration limits bool isDeregistered; - uint128 public deregistrationOpenAt; + uint128 public deregistrationOpensAt; constructor() { _disableInitializers(); @@ -51,7 +51,7 @@ contract ProvidersDelegate is IProvidersDelegate, OwnableUpgradeable { uint256 fee_, string memory name_, string memory endpoint_, - uint128 deregistrationOpenAt_ + uint128 deregistrationOpensAt_ ) external initializer { __Ownable_init(); @@ -67,7 +67,7 @@ contract ProvidersDelegate is IProvidersDelegate, OwnableUpgradeable { } fee = fee_; - deregistrationOpenAt = deregistrationOpenAt_; + deregistrationOpensAt = deregistrationOpensAt_; IERC20(token).approve(lumerinDiamond_, type(uint256).max); } @@ -248,7 +248,7 @@ contract ProvidersDelegate is IProvidersDelegate, OwnableUpgradeable { } function isDeregisterAvailable() public view returns (bool) { - return block.timestamp >= deregistrationOpenAt; + return block.timestamp >= deregistrationOpensAt; } function _deleteModelBids(bytes32[] calldata bidIds_) private { diff --git a/smart-contracts/contracts/interfaces/delegate/IDelegateFactory.sol b/smart-contracts/contracts/interfaces/delegate/IDelegateFactory.sol index c8c0f972..cd749e71 100644 --- a/smart-contracts/contracts/interfaces/delegate/IDelegateFactory.sol +++ b/smart-contracts/contracts/interfaces/delegate/IDelegateFactory.sol @@ -40,7 +40,7 @@ interface IDelegateFactory { * @param fee_ The fee percent where 100% = 10^25. * @param name_ The Subnet name. * @param endpoint_ The subnet endpoint. - * @param deregistrationOpenAt Provider deregistration will be available after this timestamp. + * @param deregistrationOpensAt_ Provider deregistration will be available after this timestamp. * @return Deployed proxy address */ function deployProxy( @@ -48,7 +48,7 @@ interface IDelegateFactory { uint256 fee_, string memory name_, string memory endpoint_, - uint128 deregistrationOpenAt + uint128 deregistrationOpensAt_ ) external returns (address); /** diff --git a/smart-contracts/contracts/interfaces/delegate/IProvidersDelegate.sol b/smart-contracts/contracts/interfaces/delegate/IProvidersDelegate.sol index 0e601818..b0975c63 100644 --- a/smart-contracts/contracts/interfaces/delegate/IProvidersDelegate.sol +++ b/smart-contracts/contracts/interfaces/delegate/IProvidersDelegate.sol @@ -93,7 +93,7 @@ interface IProvidersDelegate { * @param fee_ The fee percent where 100% = 10^25. * @param name_ The Subnet name. * @param endpoint_ The subnet endpoint. - * @param deregistrationOpenAt Provider deregistration will be available after this time. + * @param deregistrationOpensAt_ Provider deregistration will be available after this time. */ function ProvidersDelegate_init( address lumerinDiamond_, @@ -101,7 +101,7 @@ interface IProvidersDelegate { uint256 fee_, string memory name_, string memory endpoint_, - uint128 deregistrationOpenAt + uint128 deregistrationOpensAt_ ) external; /** diff --git a/smart-contracts/test/delegate/DelegateFactory.test.ts b/smart-contracts/test/delegate/DelegateFactory.test.ts index 5b6a447f..1020c040 100644 --- a/smart-contracts/test/delegate/DelegateFactory.test.ts +++ b/smart-contracts/test/delegate/DelegateFactory.test.ts @@ -111,7 +111,7 @@ describe('DelegateFactory', () => { expect(await proxy.feeTreasury()).to.eq(KYLE); expect(await proxy.name()).to.eq('name'); expect(await proxy.endpoint()).to.eq('endpoint'); - expect(await proxy.deregistrationOpenAt()).to.eq(9998887771); + expect(await proxy.deregistrationOpensAt()).to.eq(9998887771); }); it('should deploy new proxies', async () => { await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 9998887771); @@ -124,7 +124,7 @@ describe('DelegateFactory', () => { expect(await proxy.feeTreasury()).to.eq(SHEV); expect(await proxy.name()).to.eq('name2'); expect(await proxy.endpoint()).to.eq('endpoint2'); - expect(await proxy.deregistrationOpenAt()).to.eq(9998887772); + expect(await proxy.deregistrationOpensAt()).to.eq(9998887772); proxy = providersDelegateFactory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegate; expect(await proxy.owner()).to.eq(KYLE); @@ -132,7 +132,7 @@ describe('DelegateFactory', () => { expect(await proxy.feeTreasury()).to.eq(SHEV); expect(await proxy.name()).to.eq('name3'); expect(await proxy.endpoint()).to.eq('endpoint3'); - expect(await proxy.deregistrationOpenAt()).to.eq(9998887773); + expect(await proxy.deregistrationOpensAt()).to.eq(9998887773); }); it('should throw error when fee is invalid', async () => { await expect( diff --git a/smart-contracts/test/helpers/deployers/delegate/providers-delegate.ts b/smart-contracts/test/helpers/deployers/delegate/providers-delegate.ts index 56c36f00..d421fd0a 100644 --- a/smart-contracts/test/helpers/deployers/delegate/providers-delegate.ts +++ b/smart-contracts/test/helpers/deployers/delegate/providers-delegate.ts @@ -9,7 +9,7 @@ export const deployProvidersDelegate = async ( fee: BigNumberish, name: string, endpoint: string, - deregistrationOpenAt_: bigint | number, + deregistrationOpensAt_: bigint | number, ): Promise => { const [implFactory, proxyFactory] = await Promise.all([ ethers.getContractFactory('ProvidersDelegate'), @@ -20,7 +20,7 @@ export const deployProvidersDelegate = async ( const proxy = await proxyFactory.deploy(impl, '0x'); const contract = implFactory.attach(proxy) as ProvidersDelegate; - await contract.ProvidersDelegate_init(diamond, feeTreasury, fee, name, endpoint, deregistrationOpenAt_); + await contract.ProvidersDelegate_init(diamond, feeTreasury, fee, name, endpoint, deregistrationOpensAt_); return contract; }; From bade7bd93847f32a20da4fe6f6e95fdff02dfbd0 Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Mon, 30 Dec 2024 20:18:46 +0200 Subject: [PATCH 10/14] H-1 --- .../contracts/delegate/ProvidersDelegate.sol | 10 +----- .../test/delegate/ProvidersDelegate.test.ts | 32 ++++++++----------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/smart-contracts/contracts/delegate/ProvidersDelegate.sol b/smart-contracts/contracts/delegate/ProvidersDelegate.sol index 390d7e8a..a4a029c1 100644 --- a/smart-contracts/contracts/delegate/ProvidersDelegate.sol +++ b/smart-contracts/contracts/delegate/ProvidersDelegate.sol @@ -38,7 +38,6 @@ contract ProvidersDelegate is IProvidersDelegate, OwnableUpgradeable { mapping(address => Staker) public stakers; // Deregistration limits - bool isDeregistered; uint128 public deregistrationOpensAt; constructor() { @@ -119,12 +118,9 @@ contract ProvidersDelegate is IProvidersDelegate, OwnableUpgradeable { } function _stake(address staker_, uint256 amount_) private { - if (isStakeClosed) { + if (isStakeClosed && !isDeregisterAvailable()) { revert StakeClosed(); } - if (isDeregistered) { - revert ProviderDeregistered(); - } if (amount_ == 0) { revert InsufficientAmount(); } @@ -220,14 +216,10 @@ contract ProvidersDelegate is IProvidersDelegate, OwnableUpgradeable { if (!isDeregisterAvailable()) { _checkOwner(); } - if (isDeregistered) { - revert ProviderDeregistered(); - } _deleteModelBids(bidIds_); IProviderRegistry(lumerinDiamond).providerDeregister(address(this)); - isDeregistered = true; fee = 0; } diff --git a/smart-contracts/test/delegate/ProvidersDelegate.test.ts b/smart-contracts/test/delegate/ProvidersDelegate.test.ts index 5e077c52..b79518d6 100644 --- a/smart-contracts/test/delegate/ProvidersDelegate.test.ts +++ b/smart-contracts/test/delegate/ProvidersDelegate.test.ts @@ -30,7 +30,7 @@ import { import { Reverter } from '@/test/helpers/reverter'; import { setTime } from '@/utils/block-helper'; import { getProviderApproval, getReceipt } from '@/utils/provider-helper'; -import { DAY } from '@/utils/time'; +import { DAY, YEAR } from '@/utils/time'; describe('ProvidersDelegate', () => { const reverter = new Reverter(); @@ -209,6 +209,11 @@ describe('ProvidersDelegate', () => { expect(await token.balanceOf(KYLE)).to.eq(wei(900)); expect(await token.balanceOf(SHEV)).to.eq(wei(800)); }); + it('should stake tokens when stake closed but deregistration is available', async () => { + await providersDelegate.setIsStakeClosed(true); + await setTime(payoutStart + 4 * DAY); + await providersDelegate.connect(KYLE).stake(wei(100)); + }); it('should throw error when the stake is too low', async () => { await expect(providersDelegate.connect(KYLE).stake(wei(0))).to.be.revertedWithCustomError( providersDelegate, @@ -222,15 +227,6 @@ describe('ProvidersDelegate', () => { 'StakeClosed', ); }); - it('should throw error when provider deregistered', async () => { - await providersDelegate.connect(KYLE).stake(wei(1)); - await providersDelegate.providerDeregister([]); - - await expect(providersDelegate.connect(KYLE).stake(wei(1))).to.be.revertedWithCustomError( - providersDelegate, - 'ProviderDeregistered', - ); - }); }); describe('#claim', () => { @@ -403,15 +399,6 @@ describe('ProvidersDelegate', () => { 'Ownable: caller is not the owner', ); }); - it('should throw error when provider already deregistered', async () => { - await providersDelegate.connect(KYLE).stake(wei(100)); - await providersDelegate.providerDeregister([]); - - await expect(providersDelegate.providerDeregister([])).to.be.revertedWithCustomError( - providersDelegate, - 'ProviderDeregistered', - ); - }); }); describe('#postModelBid, #deleteModelBids', () => { @@ -555,6 +542,13 @@ describe('ProvidersDelegate', () => { expect(await token.balanceOf(SHEV)).to.closeTo(wei(1000), wei(0.1)); expect(await token.balanceOf(ALAN)).to.closeTo(wei(1000), wei(0.2)); expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); + + // Withdraw all stake after the first limit period + expect((await providerRegistry.getProvider(await providersDelegate.getAddress()))[1]).to.greaterThan(0); + await setTime(payoutStart + 15 * DAY + 1.1 * YEAR); + await providersDelegate.connect(KYLE).stake(wei(1)); + await providersDelegate.connect(KYLE).providerDeregister([]); + expect((await providerRegistry.getProvider(await providersDelegate.getAddress()))[1]).to.eq(0); }); }); }); From 62bdf68982f54ce273571520a88c4805d36adf39 Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Mon, 30 Dec 2024 20:27:57 +0200 Subject: [PATCH 11/14] M-2 --- smart-contracts/contracts/delegate/ProvidersDelegate.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/smart-contracts/contracts/delegate/ProvidersDelegate.sol b/smart-contracts/contracts/delegate/ProvidersDelegate.sol index a4a029c1..bbef0a7d 100644 --- a/smart-contracts/contracts/delegate/ProvidersDelegate.sol +++ b/smart-contracts/contracts/delegate/ProvidersDelegate.sol @@ -9,6 +9,7 @@ import {PRECISION} from "@solarity/solidity-lib/utils/Globals.sol"; import {IProvidersDelegate} from "../interfaces/delegate/IProvidersDelegate.sol"; import {IBidStorage} from "../interfaces/storage/IBidStorage.sol"; +import {ISessionRouter} from "../interfaces/facets/ISessionRouter.sol"; import {IProviderRegistry} from "../interfaces/facets/IProviderRegistry.sol"; import {IMarketplace} from "../interfaces/facets/IMarketplace.sol"; @@ -239,6 +240,10 @@ contract ProvidersDelegate is IProvidersDelegate, OwnableUpgradeable { _deleteModelBids(bidIds_); } + function claimForProvider(bytes32 sessionId_) external { + ISessionRouter(lumerinDiamond).claimForProvider(sessionId_); + } + function isDeregisterAvailable() public view returns (bool) { return block.timestamp >= deregistrationOpensAt; } From b18601f73721b78ed7e52b9177f018a9c038807b Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Mon, 30 Dec 2024 20:35:44 +0200 Subject: [PATCH 12/14] M-3 --- smart-contracts/contracts/delegate/ProvidersDelegate.sol | 2 ++ smart-contracts/test/delegate/ProvidersDelegate.test.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/smart-contracts/contracts/delegate/ProvidersDelegate.sol b/smart-contracts/contracts/delegate/ProvidersDelegate.sol index bbef0a7d..63e90174 100644 --- a/smart-contracts/contracts/delegate/ProvidersDelegate.sol +++ b/smart-contracts/contracts/delegate/ProvidersDelegate.sol @@ -229,6 +229,8 @@ contract ProvidersDelegate is IProvidersDelegate, OwnableUpgradeable { revert BidCannotBeCreatedDuringThisPeriod(); } + IERC20(token).safeTransferFrom(_msgSender(), address(this), IMarketplace(lumerinDiamond).getBidFee()); + return IMarketplace(lumerinDiamond).postModelBid(address(this), modelId_, pricePerSecond_); } diff --git a/smart-contracts/test/delegate/ProvidersDelegate.test.ts b/smart-contracts/test/delegate/ProvidersDelegate.test.ts index b79518d6..9d6a26f4 100644 --- a/smart-contracts/test/delegate/ProvidersDelegate.test.ts +++ b/smart-contracts/test/delegate/ProvidersDelegate.test.ts @@ -417,8 +417,10 @@ describe('ProvidersDelegate', () => { const modelId = await modelRegistry.getModelId(DELEGATOR, baseModelId); // Register bid + const balance = await token.balanceOf(providersDelegate); await providersDelegate.postModelBid(modelId, wei(0.0001)); let bidId = await marketplace.getBidId(await providersDelegate.getAddress(), modelId, 0); + expect(balance).to.eq(await token.balanceOf(providersDelegate)); await providersDelegate.deleteModelBids([bidId]); From f708007e7cad973311eda454ac8284f905fc6bff Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Mon, 30 Dec 2024 20:50:40 +0200 Subject: [PATCH 13/14] fix H-1 --- smart-contracts/contracts/delegate/ProvidersDelegate.sol | 7 ++++++- smart-contracts/test/delegate/ProvidersDelegate.test.ts | 5 ----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/smart-contracts/contracts/delegate/ProvidersDelegate.sol b/smart-contracts/contracts/delegate/ProvidersDelegate.sol index 63e90174..4f51e729 100644 --- a/smart-contracts/contracts/delegate/ProvidersDelegate.sol +++ b/smart-contracts/contracts/delegate/ProvidersDelegate.sol @@ -119,7 +119,7 @@ contract ProvidersDelegate is IProvidersDelegate, OwnableUpgradeable { } function _stake(address staker_, uint256 amount_) private { - if (isStakeClosed && !isDeregisterAvailable()) { + if (isStakeClosed && !isStakeAfterDeregisterAvailable()) { revert StakeClosed(); } if (amount_ == 0) { @@ -250,6 +250,11 @@ contract ProvidersDelegate is IProvidersDelegate, OwnableUpgradeable { return block.timestamp >= deregistrationOpensAt; } + function isStakeAfterDeregisterAvailable() public view returns (bool) { + IProviderRegistry.Provider memory provider_ = IProviderRegistry(lumerinDiamond).getProvider(address(this)); + return isDeregisterAvailable() && provider_.stake > 0 && provider_.isDeleted; + } + function _deleteModelBids(bytes32[] calldata bidIds_) private { address lumerinDiamond_ = lumerinDiamond; diff --git a/smart-contracts/test/delegate/ProvidersDelegate.test.ts b/smart-contracts/test/delegate/ProvidersDelegate.test.ts index 9d6a26f4..bd7fd718 100644 --- a/smart-contracts/test/delegate/ProvidersDelegate.test.ts +++ b/smart-contracts/test/delegate/ProvidersDelegate.test.ts @@ -209,11 +209,6 @@ describe('ProvidersDelegate', () => { expect(await token.balanceOf(KYLE)).to.eq(wei(900)); expect(await token.balanceOf(SHEV)).to.eq(wei(800)); }); - it('should stake tokens when stake closed but deregistration is available', async () => { - await providersDelegate.setIsStakeClosed(true); - await setTime(payoutStart + 4 * DAY); - await providersDelegate.connect(KYLE).stake(wei(100)); - }); it('should throw error when the stake is too low', async () => { await expect(providersDelegate.connect(KYLE).stake(wei(0))).to.be.revertedWithCustomError( providersDelegate, From 5dbb7d0bf853a813988f3b3d03971461077bb9f6 Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Mon, 6 Jan 2025 16:25:40 +0200 Subject: [PATCH 14/14] add the deployment script --- .../deploy/3_delegate_protocol.migration copy.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/smart-contracts/deploy/3_delegate_protocol.migration copy.ts b/smart-contracts/deploy/3_delegate_protocol.migration copy.ts index 0e6d452f..38bdb0cb 100644 --- a/smart-contracts/deploy/3_delegate_protocol.migration copy.ts +++ b/smart-contracts/deploy/3_delegate_protocol.migration copy.ts @@ -3,30 +3,29 @@ import { Deployer } from '@solarity/hardhat-migrate'; import { parseConfig } from './helpers/config-parser'; import { - DelegatorFactory__factory, + DelegateFactory__factory, ERC1967Proxy__factory, - ProvidersDelegator__factory, + ProvidersDelegate__factory, } from '@/generated-types/ethers'; import { wei } from '@/scripts/utils/utils'; module.exports = async function (deployer: Deployer) { const config = parseConfig(); - const providersDelegatorImpl = await deployer.deploy(ProvidersDelegator__factory); - const delegatorFactoryImpl = await deployer.deploy(DelegatorFactory__factory); + const providersDelegatorImpl = await deployer.deploy(ProvidersDelegate__factory); + const delegatorFactoryImpl = await deployer.deploy(DelegateFactory__factory); const proxy = await deployer.deploy(ERC1967Proxy__factory, [await delegatorFactoryImpl.getAddress(), '0x']); - const delegatorFactory = await deployer.deployed(DelegatorFactory__factory, await proxy.getAddress()); + const delegatorFactory = await deployer.deployed(DelegateFactory__factory, await proxy.getAddress()); - await delegatorFactory.DelegatorFactory_init(config.lumerinProtocol, providersDelegatorImpl); + await delegatorFactory.DelegateFactory_init(config.lumerinProtocol, providersDelegatorImpl, 60 * 60); await delegatorFactory.deployProxy( '0x19ec1E4b714990620edf41fE28e9a1552953a7F4', wei(0.2, 25), 'First Subnet', 'Custom endpoint', - 60 * 60 * 1, - 60 * 30, + Math.floor((Date.now() / 1000)) + 24 * 60 * 60, ); };