Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat/reward calculation #59

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
liquid-staking/node_modules/
liquid-staking/dist/
liquid-staking/cache/
liquid-staking/artifacts/
liquid-staking/coverage/
liquid-staking/artifacts/
41 changes: 41 additions & 0 deletions liquid-staking/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Build artifacts
artifacts/
cache/
.alephium/

# Dependencies
node_modules/
package-lock.json
yarn.lock

# Environment files
.env
.env.*
!.env.example

# IDE files
.vscode/
.idea/
*.iml
*.iws
*.ipr

# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# System files
.DS_Store
Thumbs.db

# Test coverage
coverage/
.nyc_output/

# Temporary files
*.swp
*.swo
*~
85 changes: 85 additions & 0 deletions liquid-staking/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Liquid Staking Contract

A smart contract implementation for liquid staking on Alephium blockchain.

## Overview

This contract implements a liquid staking system with the following features:

- Token staking with flexible amounts
- Time-based reward multipliers
- Performance-based reward adjustments
- Compound staking support
- Flexible withdrawal system

## Contract Features

### Staking

- Users can stake tokens at any time
- Multiple stakes from the same user are combined
- Automatic reward calculation based on stake duration

### Rewards

- Base reward rate adjustable by owner
- Time-based multipliers:
- 100% base rate for < 7 days
- 105% for 7-30 days
- 110% for > 30 days
- Pool performance multiplier (adjustable by owner)
- Rewards are calculated and distributed in a separate reward token

### Withdrawal

- Flexible withdrawal amounts
- Automatic reward claiming during withdrawal
- Complete or partial withdrawal options

## Contract Structure

### State Variables

- `tokenId`: The staking token identifier
- `rewardTokenId`: The reward token identifier
- `baseRewardRate`: Annual reward rate in basis points
- `poolPerformanceMultiplier`: Performance multiplier in basis points
- `owner`: Contract owner address

### Key Functions

- `stake(amount: U256)`: Stake tokens
- `withdraw(amount: U256)`: Withdraw staked tokens
- `claimRewards()`: Claim accumulated rewards
- `updatePoolPerformance(newMultiplier: U256)`: Update pool performance multiplier

## Development

### Prerequisites

- Node.js (version specified in package.json)
- Alephium development environment

### Setup

```bash
# Install dependencies
npm install

# Build contracts
npm run build

# Run tests
npm test
```

### Error Codes

- `InvalidAmount` (0): Invalid amount specified
- `NoStakeFound` (1): No stake found for the caller
- `NoRewardsToClaim` (2): No rewards available to claim
- `InvalidMultiplier` (3): Invalid performance multiplier value

## License

[License Type]
20 changes: 20 additions & 0 deletions liquid-staking/alephium.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Configuration } from '@alephium/cli'

const configuration: Configuration = {
networks: {
devnet: {
nodeUrl: 'http://127.0.0.1:12973',
privateKeys: ['a642942e67258589cd2b1822c631506632db5a12aabcf413604e785300d762a5'],
apiKey: '0000000000000000000000000000000000000000000000000000000000000000',
networkId: 4,
confirmations: 1,
settings: {
miningEnabled: true,
autoMine: true,
networkType: 'devnet'
}
}
}
}

export default configuration
Binary file added liquid-staking/bun.lockb
Binary file not shown.
143 changes: 143 additions & 0 deletions liquid-staking/contracts/reward_system.ral
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
Contract RewardSystem(
tokenId: ByteVec,
rewardTokenId: ByteVec,
mut baseRewardRate: U256, // Annual rate in basis points (1% = 100)
mut poolPerformanceMultiplier: U256, // In basis points (100% = 10000)
owner: Address
) {
// Stake information for each user
pub struct StakeInfo {
amount: U256, // Amount of tokens staked
startTime: U256, // Initial stake timestamp
lastClaimTime: U256 // Last reward claim timestamp
}

// Mapping to store stake info for each address
mapping[Address => StakeInfo] mut stakes

// Constants for calculations (in basis points)
const BasisPoints: U256 = 10000
const MaxMultiplier: U256 = 20000 // 200%
const SecondsInYear: U256 = 31536000 // 365 * 24 * 60 * 60

@using(preapprovedAssets = true)
pub fn stake(amount: U256) -> () {
let caller = callerAddress!()
assert!(amount > 0, ErrorCodes.InvalidAmount)

transferToken!(caller, selfAddress!(), tokenId, amount)

let currentTime = blockTimeStamp!()
let stakeInfo = if (stakes.contains(caller)) {
let existing = stakes[caller]
StakeInfo {
amount: existing.amount + amount,
startTime: existing.startTime,
lastClaimTime: currentTime
}
} else {
StakeInfo {
amount: amount,
startTime: currentTime,
lastClaimTime: currentTime
}
}

stakes[caller] = stakeInfo
emit Stake(caller, amount)
}

@using(preapprovedAssets = true)
pub fn withdraw(amount: U256) -> () {
let caller = callerAddress!()
assert!(stakes.contains(caller), ErrorCodes.NoStakeFound)
let stakeInfo = stakes[caller]
assert!(amount > 0 && amount <= stakeInfo.amount, ErrorCodes.InvalidAmount)

// Claim any pending rewards before withdrawal
let rewards = calculateReward(caller)
if (rewards > 0) {
transferToken!(selfAddress!(), caller, rewardTokenId, rewards)
emit RewardClaimed(caller, rewards)
}

// Process withdrawal
transferToken!(selfAddress!(), caller, tokenId, amount)

let newAmount = stakeInfo.amount - amount
if (newAmount == 0) {
removeFromMap!(stakes, caller)
} else {
let newStakeInfo = StakeInfo {
amount: newAmount,
startTime: stakeInfo.startTime,
lastClaimTime: blockTimeStamp!()
}
stakes[caller] = newStakeInfo
}

emit Withdraw(caller, amount)
}

pub fn claimRewards() -> () {
let caller = callerAddress!()
assert!(stakes.contains(caller), ErrorCodes.NoStakeFound)
let stakeInfo = stakes[caller]

let reward = calculateReward(caller)
assert!(reward > 0, ErrorCodes.NoRewardsToClaim)

transferToken!(selfAddress!(), caller, rewardTokenId, reward)

let newStakeInfo = StakeInfo {
amount: stakeInfo.amount,
startTime: stakeInfo.startTime,
lastClaimTime: blockTimeStamp!()
}
stakes[caller] = newStakeInfo

emit RewardClaimed(caller, reward)
}

fn calculateReward(staker: Address) -> U256 {
let stakeInfo = stakes[staker]
let timeElapsed = blockTimeStamp!() - stakeInfo.lastClaimTime

// Base reward calculation
let baseReward = (stakeInfo.amount * baseRewardRate * timeElapsed) / (SecondsInYear * BasisPoints)

// Apply time-based multiplier
let stakeDuration = blockTimeStamp!() - stakeInfo.startTime
let timeMultiplier = if (stakeDuration >= 30 days!()) {
11000 // 110%
} else if (stakeDuration >= 7 days!()) {
10500 // 105%
} else {
10000 // 100%
}

// Apply pool performance multiplier and time multiplier
return (baseReward * timeMultiplier * poolPerformanceMultiplier) / (BasisPoints * BasisPoints)
}

pub fn updatePoolPerformance(newMultiplier: U256) -> () {
checkCaller!(callerAddress!() == owner)
assert!(newMultiplier > 0 && newMultiplier <= MaxMultiplier, ErrorCodes.InvalidMultiplier)
poolPerformanceMultiplier = newMultiplier
emit PoolPerformanceUpdated(newMultiplier)
}

// Error codes
const ErrorCodes = {
InvalidAmount: 0,
NoStakeFound: 1,
NoRewardsToClaim: 2,
InvalidMultiplier: 3
}

// Events
event Stake(staker: Address, amount: U256)
event Withdraw(staker: Address, amount: U256)
event RewardClaimed(staker: Address, amount: U256)
event PoolPerformanceUpdated(multiplier: U256)
}
18 changes: 18 additions & 0 deletions liquid-staking/contracts/staking.ral
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Contract Staking(
// ... existing fields
rewardSystemId: ByteVec
) {
// ... existing code

@using(updateFields = true)
pub fn stake(amount: U256) -> () {
// ... existing stake logic
updateRewards(callerAddress!())
}

@using(updateFields = true)
pub fn unstake(amount: U256) -> () {
// ... existing unstake logic
distributeRewards(callerAddress!())
}
}
28 changes: 28 additions & 0 deletions liquid-staking/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "liquid-staking",
"version": "1.0.0",
"scripts": {
"test": "jest",
"build": "npx @alephium/cli compile",
"devnet": "bash start-devnet.sh",
"docker:up": "docker-compose up --build",
"docker:down": "docker-compose down",
"docker:build": "docker-compose run app npm run build"
},
"dependencies": {
"@alephium/web3": "0.19.2",
"@alephium/cli": "0.19.2"
},
"devDependencies": {
"@types/jest": "^29.5.0",
"jest": "^29.5.0",
"ts-jest": "^29.1.0",
"typescript": "^5.0.4",
"@alephium/web3-test": "0.19.2",
"@types/node": "^18.0.0",
"ts-node": "^10.9.1"
},
"engines": {
"node": ">=16.0.0 <21.0.0"
}
}
Loading