Skip to content

Commit

Permalink
Merge pull request #24 from Lumerin-protocol/main
Browse files Browse the repository at this point in the history
Weekly update 20240905
  • Loading branch information
rcondron authored Sep 9, 2024
2 parents b867209 + e568e61 commit 6cb077e
Show file tree
Hide file tree
Showing 107 changed files with 13,711 additions and 453 deletions.
10 changes: 6 additions & 4 deletions docs/00-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ In other words, referring to the overview model...how do we get to conversation
Numbers below reference the circled elements in the diagram above.

## 0. Existing Foundation Elements
- [Readme](../README.md) - for more details
- [Readme](../readme.md) - for more details
- Arbitrum Ethereum Layer 2 blockchain
- Morpheus Token (MOR) for staking and bidding
- Lumerin Smart Contract for governance and routing
Expand All @@ -38,7 +38,6 @@ Numbers below reference the circled elements in the diagram above.
1. Register your provider (the proxy-router) on the blockchain (http://mycoolproxy.serverfarm.io:3333)
1. Register your model on the blockchain
1. Create a bid for your model on the blockchain
- Further details on how to do this are in the [Provider Offer Guide](provider-offer.md)

## 4. Consumer Node Setup
- [04-consumer-setup.md](04-consumer-setup.md) - for more details
Expand All @@ -53,10 +52,13 @@ Numbers below reference the circled elements in the diagram above.
- The consumer node will need to have the proxy-router running and the UI-Desktop running to interact with the models and bids on the blockchain

## 5. Purchase Bid
- [05-purchase-bid.md](05-purchase-bid.md) - for more details
- [05-bid-purchase.md](05-bid-purchase.md) - for more details
- Once the UI-Desktop is up and running, the consumer can browse the available bids on the blockchain
- Select a bid and stake the intended MOR amount (minimum should be shown)

## 6. Prompt & Inference
- [06-model-interaction.md](06-model-interaction.md) - for more details
- Once the bid is purchased, the consumer can send prompts to the proxy-router via the UI-Desktop
- Once the bid is purchased, the consumer can send prompts to the proxy-router via the UI-Desktop

## Proxy-Router and Possible LLM Server Configurations - Reference Architecture
![Reference Architecture](images/system-architecture.png)
Binary file added docs/audits/staking/LMR-MOR_Staking_20240827.pdf
Binary file not shown.
Binary file added docs/images/system-architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docs/images/system-architecture.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
770 changes: 443 additions & 327 deletions docs/source/overview.drawio

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion smart-contracts/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
node_modules
.env
*.env

# Hardhat files
/cache
Expand Down
72 changes: 55 additions & 17 deletions smart-contracts/contracts/StakingMasterChef.sol
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

// Refer to https://www.rareskills.io/post/staking-algorithm
contract StakingMasterChef is Ownable {
using SafeERC20 for IERC20;

struct Pool {
uint256 rewardPerSecondScaled; // reward tokens per second, times `PRECISION`
uint256 lastRewardTime; // last time rewards were distributed
uint256 accRewardPerShareScaled; // accumulated reward per share, times `PRECISION`
uint256 totalShares; // total shares of reward token
uint256 totalStaked; // total staked tokens
uint256 startTime; // start time of the staking for this pool
uint256 endTime; // end time of the staking for this pool - after this time, no more rewards will be distributed
uint256 undistributedReward; // reward that was not distributed
Lock[] locks; // locks available for this pool: durations with corresponding multipliers
}

Expand All @@ -25,6 +30,7 @@ contract StakingMasterChef is Ownable {
uint256 stakeAmount; // amount of staked tokens
uint256 shareAmount; // shares received after staking
uint256 rewardDebt; // reward debt
uint256 stakedAt; // time when stake was added
uint256 lockEndsAt; // when staking lock duration ends
}

Expand All @@ -43,11 +49,11 @@ contract StakingMasterChef is Ownable {
event PoolStopped(uint256 indexed poolId);

error PoolOrStakeNotExists();
error StakingNotStarted();
error StakingFinished();
error LockNotEnded();
error LockReleaseTimePastPoolEndTime(); // lock duration exceeds staking range, choose a shorter lock duration
error NoRewardAvailable();
error ZeroStake(); // stake amount is zero

constructor(IERC20 _stakingToken, IERC20 _rewardToken) Ownable(_msgSender()) {
stakingToken = _stakingToken;
Expand Down Expand Up @@ -76,12 +82,14 @@ contract StakingMasterChef is Ownable {
rewardPerSecondScaled: (_totalReward * PRECISION) / _duration,
locks: _lockDurations,
accRewardPerShareScaled: 0,
totalShares: 0
totalShares: 0,
totalStaked: 0,
undistributedReward: 0
})
);
emit PoolAdded(poolId, _startTime, endTime);

rewardToken.transferFrom(_msgSender(), address(this), _totalReward);
rewardToken.safeTransferFrom(_msgSender(), address(this), _totalReward);

return poolId;
}
Expand All @@ -93,17 +101,35 @@ contract StakingMasterChef is Ownable {
return pools[_poolId].locks;
}

function getPoolsCount() external view returns (uint256) {
return pools.length;
}

/// @notice Stops the pool, no more rewards will be distributed
/// @param _poolId the id of the pool
function stopPool(uint256 _poolId) external onlyOwner poolExists(_poolId) {
Pool storage pool = pools[_poolId]; // errors if poolId is invalid
Pool storage pool = pools[_poolId];
_recalculatePoolReward(pool);
uint256 oldEndTime = pool.endTime;

if (block.timestamp >= pool.endTime) {
revert StakingFinished();
}

uint256 futureUndistributedReward = ((pool.endTime - block.timestamp) * pool.rewardPerSecondScaled) / PRECISION;
pool.undistributedReward += futureUndistributedReward;
pool.endTime = block.timestamp;

emit PoolStopped(_poolId);
}

uint256 undistributedReward = ((oldEndTime - block.timestamp) * pool.rewardPerSecondScaled) / PRECISION;
safeTransfer(_msgSender(), undistributedReward);
/// @notice Withdraw undistributed reward, can be called before the pool is stopped if there is a period without active stakes
/// @param _poolId the id of the pool
function withdrawUndistributedReward(uint256 _poolId) external onlyOwner poolExists(_poolId) {
Pool storage pool = pools[_poolId];
_recalculatePoolReward(pool);
uint256 reward = pool.undistributedReward;
pool.undistributedReward = 0;
safeTransfer(_msgSender(), reward);
}

/// @notice Manually update pool reward variables
Expand All @@ -120,7 +146,9 @@ contract StakingMasterChef is Ownable {
return;
}

if (_pool.totalShares != 0) {
if (_pool.totalShares == 0) {
_pool.undistributedReward += ((timestamp - _pool.lastRewardTime) * _pool.rewardPerSecondScaled) / PRECISION;
} else {
_pool.accRewardPerShareScaled = getRewardPerShareScaled(_pool, timestamp);
}

Expand All @@ -139,15 +167,15 @@ contract StakingMasterChef is Ownable {
/// @param _lockId the id for the predefined lock duration of the pool, earlier withdrawal is not possible
/// @return stakeId the id of the new stake
function stake(uint256 _poolId, uint256 _amount, uint8 _lockId) external poolExists(_poolId) returns (uint256) {
Pool storage pool = pools[_poolId];
if (block.timestamp < pool.startTime) {
revert StakingNotStarted();
if (_amount == 0) {
revert ZeroStake();
}
Pool storage pool = pools[_poolId];
if (block.timestamp >= pool.endTime) {
revert StakingFinished();
}
Lock storage lock = pool.locks[_lockId];
uint256 lockEndsAt = block.timestamp + lock.durationSeconds;
uint256 lockEndsAt = max(block.timestamp, pool.startTime) + lock.durationSeconds;
if (lockEndsAt > pool.endTime) {
revert LockReleaseTimePastPoolEndTime();
}
Expand All @@ -156,6 +184,7 @@ contract StakingMasterChef is Ownable {

uint256 userShares = (_amount * lock.multiplierScaled) / PRECISION;
pool.totalShares += userShares;
pool.totalStaked += _amount;

UserStake[] storage userStakes = poolUserStakes[_poolId][_msgSender()];
uint256 stakeId = userStakes.length;
Expand All @@ -164,13 +193,13 @@ contract StakingMasterChef is Ownable {
stakeAmount: _amount,
shareAmount: userShares,
rewardDebt: (userShares * pool.accRewardPerShareScaled) / PRECISION,
stakedAt: block.timestamp,
lockEndsAt: lockEndsAt
})
);

emit Stake(_msgSender(), _poolId, stakeId, _amount);
stakingToken.transferFrom(address(_msgSender()), address(this), _amount);

stakingToken.safeTransferFrom(address(_msgSender()), address(this), _amount);
return stakeId;
}

Expand All @@ -187,7 +216,7 @@ contract StakingMasterChef is Ownable {

// lockEndsAt cannot be larger than pool.endTime if stopPool is not called
// if stopPool is called, lockEndsAt is not checked
if (block.timestamp < min(pool.endTime, userStake.lockEndsAt)) {
if ((block.timestamp > pool.startTime) && (block.timestamp < min(pool.endTime, userStake.lockEndsAt))) {
revert LockNotEnded();
}

Expand All @@ -197,6 +226,7 @@ contract StakingMasterChef is Ownable {
uint256 reward = (userStake.shareAmount * pool.accRewardPerShareScaled) / PRECISION - userStake.rewardDebt;

pool.totalShares -= userStake.shareAmount;
pool.totalStaked -= unstakeAmount;

userStake.rewardDebt = 0;
userStake.stakeAmount = 0;
Expand All @@ -206,7 +236,7 @@ contract StakingMasterChef is Ownable {
emit Unstake(_msgSender(), _poolId, _stakeId, unstakeAmount);

safeTransfer(_msgSender(), reward);
stakingToken.transfer(address(_msgSender()), unstakeAmount);
stakingToken.safeTransfer(address(_msgSender()), unstakeAmount);
}

/// @notice Get stake of a user in a pool
Expand Down Expand Up @@ -253,6 +283,10 @@ contract StakingMasterChef is Ownable {
}

Pool storage pool = pools[_poolId];
if (block.timestamp < pool.startTime) {
return 0;
}

uint256 timestamp = min(block.timestamp, pool.endTime);
return (userStake.shareAmount * getRewardPerShareScaled(pool, timestamp)) / PRECISION - userStake.rewardDebt;
}
Expand Down Expand Up @@ -283,7 +317,11 @@ contract StakingMasterChef is Ownable {
/// @dev Safe reward transfer function, just in case if rounding error causes pool to not have enough reward token.
function safeTransfer(address _to, uint256 _amount) private {
uint256 rewardBalance = rewardToken.balanceOf(address(this));
rewardToken.transfer(_to, min(rewardBalance, _amount));
rewardToken.safeTransfer(_to, min(rewardBalance, _amount));
}

function max(uint256 _a, uint256 _b) private pure returns (uint256) {
return _a > _b ? _a : _b;
}

function min(uint256 _a, uint256 _b) private pure returns (uint256) {
Expand Down
23 changes: 15 additions & 8 deletions smart-contracts/contracts/facets/SessionRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -357,16 +357,23 @@ contract SessionRouter {
/// @notice returns amount of withdrawable user stake and one on hold
function withdrawableUserStake(
address userAddr,
uint8 iterations
uint256 offset,
uint8 limit
) external view returns (uint256 avail, uint256 hold) {
OnHold[] memory onHold = s.userOnHold[userAddr];
iterations = iterations > onHold.length ? uint8(onHold.length) : iterations;
for (uint i = 0; i < onHold.length; i++) {
uint256 amount = onHold[i].amount;
if (block.timestamp < onHold[i].releaseAt) {
hold += amount;
OnHold[] storage onHold = s.userOnHold[userAddr];

uint256 length = onHold.length;
if (length <= offset) {
return (avail, hold);
}

uint8 size = offset + limit > length ? uint8(length - offset) : limit;
for (uint i = offset; i < offset + size; i++) {
OnHold storage hh = onHold[i];
if (block.timestamp < hh.releaseAt) {
hold += hh.amount;
} else {
avail += amount;
avail += hh.amount;
}
}
return (avail, hold);
Expand Down
10 changes: 8 additions & 2 deletions smart-contracts/hardhat-mainnet.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import baseConfig from "./hardhat.config";
import { HardhatUserConfig } from "hardhat/types";
import type { HardhatUserConfig } from "hardhat/types";

if (!process.env.ETH_NODE_ADDRESS) {
throw new Error("ETH_NODE_ADDRESS env variable is not set");
Expand All @@ -13,19 +13,25 @@ if (!process.env.ETHERSCAN_API_KEY) {
throw new Error("ETHERSCAN_API_KEY env variable is not set");
}

if (!process.env.CHAIN_ID) {
throw new Error("CHAIN_ID env variable is not set");
}

const config: HardhatUserConfig = {
...baseConfig,
networks: {
...baseConfig.networks,
default: {
chainId: 421614,
chainId: Number(process.env.CHAIN_ID),
url: process.env.ETH_NODE_ADDRESS,
accounts: [process.env.OWNER_PRIVATE_KEY],
},
},
etherscan: {
apiKey: {
sepolia: process.env.ETHERSCAN_API_KEY,
arbitrumSepolia: process.env.ETHERSCAN_API_KEY,
sepolia: process.env.ETHERSCAN_API_KEY,
},
},
sourcify: {
Expand Down
7 changes: 6 additions & 1 deletion smart-contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import "@nomicfoundation/hardhat-toolbox-viem";
import "@nomicfoundation/hardhat-ignition-viem";
import "@solarity/hardhat-gobind";
import "./tasks/upgrade";
import { HardhatUserConfig } from "hardhat/config";
import type { HardhatUserConfig } from "hardhat/config";

const config: HardhatUserConfig = {
networks: {
hardhat: {
initialDate: "2024-07-16T01:00:00.000Z",
gas: "auto", // required for tests where two transactions should be mined in the same block
// loggingEnabled: true,
mining: {
auto: true,
interval: 10_000,
},
},
},
solidity: {
Expand Down Expand Up @@ -44,6 +48,7 @@ const config: HardhatUserConfig = {
// L2: "arbitrum",
L1Etherscan: process.env.ETHERSCAN_API_KEY,
L1: "ethereum",
reportPureAndViewMethods: true,
},
gobind: {
outdir: "./bindings/go",
Expand Down
2 changes: 1 addition & 1 deletion smart-contracts/node-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ stop_hardhat() {
}

# Start the hardhat process in the background
$BIN_PATH/hardhat node &
$BIN_PATH/hardhat node --show-stack-traces &
HARDHAT_PID=$!
echo "Started hardhat with PID $HARDHAT_PID"

Expand Down
8 changes: 6 additions & 2 deletions smart-contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
"report": "open ./coverage/index.html",
"verify": "hardhat verify",
"bindings-go": "hardhat gobind",
"bindings-ts": "wagmi generate",
"node-local": "./node-local.sh",
"format": "prettier --write \"contracts/**/*.sol\"",
"lint": "solhint \"contracts/[!diamond]*/[!Test]*.sol\"",
"copy-bindings": "cp -r bindings/go/contracts/facets/* ../proxy-router/contracts"
"copy-bindings": "cp -r bindings/go/contracts/facets/* ../proxy-router/contracts",
"copy-bindings-ts": "cp -r bindings/ts/abi.ts ../ui-staking/src/blockchain/abi.ts"
},
"devDependencies": {
"@nomicfoundation/hardhat-ignition": "^0.15.2",
Expand Down Expand Up @@ -46,5 +48,7 @@
"viem": "^2.10.1",
"yargs-parser": "^21.1.1"
},
"dependencies": {}
"dependencies": {
"@wagmi/cli": "^2.1.15"
}
}
Loading

0 comments on commit 6cb077e

Please sign in to comment.