Skip to content

Commit dbc4eb9

Browse files
authored
Implement loyalty reward program contracts (#9)
* implement deposit_pool and reward_pool for loyalty reward program * define loyalty token * unify variables/methods naming * update deploy script and documents
1 parent 3f15a7a commit dbc4eb9

File tree

14 files changed

+1843
-115
lines changed

14 files changed

+1843
-115
lines changed

README.md

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Talus Token Project
22

3-
This repository contains the smart contracts for the Talus Token project on Sui blockchain, featuring a custom token and a decentralized faucet for token distribution.
3+
This repository contains the smart contracts for the Talus Token project on Sui blockchain, featuring a custom token, a decentralized faucet for token distribution, a deposit pool for yield generation, and a reward pool for loyalty incentives.
44

55
## Prerequisites
66

@@ -13,8 +13,10 @@ This repository contains the smart contracts for the Talus Token project on Sui
1313

1414
```
1515
talus-token/
16-
├── talus/ # Talus token implementation
16+
├── talus/ # US token (Coin)
1717
├── faucet/ # Bi-directional faucet contract
18+
├── loyalty/ # Loyalty token
19+
├── deposit_pool/ # Deposit pool and reward pool contracts
1820
└── deploy.sh # Deployment script
1921
```
2022

@@ -26,18 +28,66 @@ A custom token implementation on the Sui blockchain.
2628
### Faucet Module
2729
The faucet module implements a faucet that enables exchanging between target token (e.g. TALUS) and base token (e.g. SUI) at a configurable exchange rate. Key features include:
2830

29-
- Configurable exchange rate between two token for test net so the Sybil attack resistance is based on supply of base token
31+
- Configurable exchange rate between two tokens for test net so the Sybil attack resistance is based on supply of base token
3032
- Percentage-based withdrawal limits to prevent draining
3133
- Ability to inject additional liquidity
3234
- Simple interface for minting and refunding
3335

36+
### Deposit Pool Module
37+
38+
The deposit pool module allows users to deposit base tokens and earn loyalty tokens as rewards. Users can lock their tokens for different time periods with varying APY rates. Key features include:
39+
40+
- Multiple lock terms with configurable APY
41+
- Early withdrawal support (configurable)
42+
- Pending withdrawal period (optional)
43+
- Admin-controlled reward pool integration
44+
- Receipts for each deposit, enabling precise reward calculation
45+
46+
#### Usage
47+
48+
```move
49+
// Initialize a deposit pool
50+
let treasury_cap = // ... obtain Loyalty token treasury cap
51+
let base_apy = 5; // 5% APY
52+
let early_withdrawal = true;
53+
let withdrawal_pending_days = 2;
54+
deposit_pool::deposit_pool::initiate<Base, Loyalty>(
55+
treasury_cap, base_apy, early_withdrawal, withdrawal_pending_days, ctx
56+
);
57+
58+
// Deposit base tokens
59+
deposit_pool::deposit_pool::deposit(pool, base_coin, term_days, clock, recipient, ctx);
60+
61+
// Withdraw and claim rewards
62+
deposit_pool::deposit_pool::withdrawal(pool, receipt, clock, ctx);
63+
```
64+
65+
### Reward Pool Module
66+
67+
The reward pool module manages reward pools and allows users to claim rewards by spending loyalty tokens. Pools can be refreshed with more rewards, and events are emitted for transparency.
68+
69+
#### Usage
70+
71+
```move
72+
// Create a new reward pool
73+
let reward_coin = // ... obtain reward tokens
74+
let rate = 10; // 10 Loyalty tokens per reward token
75+
deposit_pool::reward_program::new_reward_pool<Loyalty, Reward>(reward_coin, rate, ctx);
76+
77+
// Add more rewards to the pool
78+
deposit_pool::reward_program::reward_fresh(pool, additional_reward_coin);
79+
80+
// Claim rewards by spending loyalty tokens
81+
deposit_pool::reward_program::claim(pool, loyalty_token, policy, ctx);
82+
```
83+
3484
## Deployment
3585

3686
The project includes an automated deployment script that:
3787
1. Starts a local Sui node if remote rpc is not provided
3888
2. Sets up the environment
39-
3. Publishes both contracts
40-
4. Initializes the faucet with initial liquidity
89+
3. Publishes all contracts
90+
4. Initializes the faucet and deposit pool with initial liquidity
4191

4292
To deploy:
4393
```bash
@@ -79,16 +129,17 @@ faucet::refund(faucet, talus_coin, ctx);
79129
faucet::inject(faucet, additional_talus, ctx);
80130
```
81131

82-
### Security Features
132+
## Security Features
83133

84-
The faucet includes several security measures:
134+
The contracts include several security measures:
85135
- Withdrawal limits (configurable percentage) prevent large withdrawals
86136
- Fixed exchange rates prevent manipulation
87137
- Shared object model ensures equal access
88138
- Idempotent deployment process
89139
- Retry mechanisms for faucet operations
140+
- Admin controls for deposit pool and reward pool configuration
90141

91-
### Configuration
142+
## Configuration
92143

93144
Default values in deployment:
94145
- Total Supply: 10^19 tokens

deploy.sh

Lines changed: 170 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,36 @@
11
#!/bin/bash
2-
SUI="sui"
3-
TOTAL_AMOUNT="10^19"
4-
5-
# Helper function for large number calculations
6-
_calculate() {
7-
echo "scale=0; $1" | bc
8-
}
92

10-
DEFAULT_INIT=$(_calculate "$TOTAL_AMOUNT/2")
3+
set -euo pipefail
114

12-
# Add user input for RPC and alias
13-
read -p "Enter RPC URL (default: http://127.0.0.1:9000): " RPC_URL
14-
read -p "Enter environment alias (default: local): " ENV_ALIAS
15-
read -p "Enter initial amount (default: $DEFAULT_INIT (half)): " INIT_AMOUNT
16-
read -p "Enter exhcnage rate Talus/Sui (default: 10): " EXCHANGE_RATE
17-
read -p "Enter max withdrawal ratio every time (default: 50 (0~100)): " WITHDRAWAL_PCT
18-
read -p "Deploy and initialize faucet? (y/N): " DEPLOY_FAUCET
19-
20-
21-
# Set default values if no input provided
22-
RPC_URL=${RPC_URL:-"http://127.0.0.1:9000"}
23-
ENV_ALIAS=${ENV_ALIAS:-"local"}
24-
EXCHANGE_RATE=${EXCHANGE_RATE:-10}
25-
WITHDRAWAL_PCT=${WITHDRAWAL_PCT:-50}
26-
DEPLOY_FAUCET=${DEPLOY_FAUCET:-"n"}
5+
###########################################
6+
# Configuration Variables
7+
###########################################
8+
SUI="sui"
9+
TOTAL_SUPPLY="10^19" # Changed from TOTAL_AMOUNT
10+
INITIAL_SPLIT_AMOUNT=0 # Changed from SPLIT_AMOUNT
11+
BASE_APY=2
2712

28-
if [ -z "$INIT_AMOUNT" ]; then
29-
SPLIT_AMOUNT=$DEFAULT_INIT
30-
else
31-
SPLIT_AMOUNT=$(_calculate "$TOTAL_AMOUNT-$INIT_AMOUNT")
32-
fi
13+
###########################################
14+
# Helper Functions
15+
###########################################
3316

34-
# Setup client if needed
35-
if ! $SUI client active-env | grep -q "$ENV_ALIAS"; then
36-
echo "Setting up client with RPC: $RPC_URL and alias: $ENV_ALIAS"
37-
$SUI client new-env --alias "$ENV_ALIAS" --rpc "$RPC_URL"
38-
$SUI client switch --env "$ENV_ALIAS"
39-
fi
17+
# Calculate large numbers using bc
18+
_calculate_amount() { # Changed from _calculate
19+
echo "scale=0; $1" | bc
20+
}
4021

4122
# Helper functions for idempotency
4223
_checkProcess() {
4324
pgrep -f "$1" >/dev/null
4425
return $?
4526
}
4627

47-
# Run a command in the background.
28+
# Run a command in the background
4829
_evalBg() {
4930
eval "$@" &>/dev/null & disown;
5031
}
5132

52-
53-
# Get faucet coins if needed with retry
33+
# Get faucet coins with retry mechanism
5434
_getFaucetCoins() {
5535
local max_attempts=5
5636
local attempt=1
@@ -80,7 +60,40 @@ _getFaucetCoins() {
8060
done
8161
}
8262

83-
# Start node if not running
63+
###########################################
64+
# User Input Configuration
65+
###########################################
66+
67+
# Calculate default initialization amount
68+
DEFAULT_INIT=$(_calculate_amount "$TOTAL_SUPPLY/2")
69+
70+
# Collect user inputs with descriptive prompts
71+
read -p "Enter RPC URL (default: http://127.0.0.1:9000): " RPC_URL
72+
read -p "Enter environment alias (default: local): " ENV_ALIAS
73+
read -p "Enter faucet source size (default: $DEFAULT_INIT (half)): " INIT_AMOUNT
74+
read -p "Enter exchange rate Talus/Sui (default: 10): " EXCHANGE_RATE
75+
read -p "Enter max withdrawal ratio every time (default: 50 (0~100)): " WITHDRAWAL_PCT
76+
read -p "Deploy and initialize faucet? (y/N): " DEPLOY_FAUCET
77+
78+
# Set default values for configuration
79+
RPC_URL=${RPC_URL:-"http://127.0.0.1:9000"}
80+
ENV_ALIAS=${ENV_ALIAS:-"local"}
81+
EXCHANGE_RATE=${EXCHANGE_RATE:-10}
82+
WITHDRAWAL_PCT=${WITHDRAWAL_PCT:-50}
83+
DEPLOY_FAUCET=${DEPLOY_FAUCET:-"n"}
84+
85+
###########################################
86+
# Environment Setup
87+
###########################################
88+
89+
# Configure Sui client environment if not already set
90+
if ! $SUI client active-env | grep -q "$ENV_ALIAS"; then
91+
echo "Setting up client with RPC: $RPC_URL and alias: $ENV_ALIAS"
92+
$SUI client new-env --alias "$ENV_ALIAS" --rpc "$RPC_URL"
93+
$SUI client switch --env "$ENV_ALIAS"
94+
fi
95+
96+
# Start local node if not running
8497
if ! _checkProcess "$SUI start"; then
8598
echo "Starting Node"
8699
start_node="RUST_LOG=\"off,sui_node=error\" $SUI start --with-faucet --force-regenesis"
@@ -91,49 +104,152 @@ else
91104
echo "Node already running"
92105
fi
93106

107+
###########################################
108+
# Token Deployment
109+
###########################################
110+
111+
# Get active address
94112
echo "get address"
95113
USER=$($SUI client active-address)
96114
echo "Active address: \"$USER\""
97115

98-
# Get faucet coins if needed
116+
# Ensure we have gas
99117
if ! $SUI client gas | grep -q "0x"; then
100118
_getFaucetCoins
101119
fi
102120

121+
# Deploy main token contract
103122
echo "Publishing Token contract:"
104-
TokenContractID=$($SUI client publish --gas-budget 300000000 ./talus --json| jq -r ".objectChanges[] | select(.packageId) | .packageId")
105-
TalusCoin=$($SUI client balance --with-coins --json | jq -r '.[0][][1][] | select(.coinType | contains("::us::US")) | .coinObjectId')
123+
TOKEN_CONTRACT_ID=$($SUI client publish ./talus --json| jq -er ".objectChanges[] | select(.packageId) | .packageId")
124+
TALUS_COIN=$($SUI client balance --with-coins --json | jq -er '.[0][][1][] | select(.coinType | contains("::us::US")) | .coinObjectId')
106125
sleep 3
107-
echo "Token Contract at: \"$TokenContractID\""
108-
echo "Talus coin at : \"$TalusCoin\""
126+
echo "Token Contract at: \"$TOKEN_CONTRACT_ID\""
127+
echo "Talus coin at : \"$TALUS_COIN\""
109128

110-
echo "Split coin"
111-
splitres=$($SUI client split-coin --coin-id $TalusCoin --amounts $SPLIT_AMOUNT --gas-budget 10000000)
129+
###########################################
130+
# Faucet Deployment (Optional)
131+
###########################################
112132

113-
# Only deploy faucet if requested
114133
if [[ "${DEPLOY_FAUCET,,}" =~ ^(y|yes)$ ]]; then
134+
# Calculate split amounts for faucet
135+
if [ -z "$INIT_AMOUNT" ]; then
136+
SPLIT_AMOUNT=$DEFAULT_INIT
137+
else
138+
SPLIT_AMOUNT=$(_calculate_amount "$TOTAL_SUPPLY-$INIT_AMOUNT")
139+
fi
140+
141+
echo "Split coin"
142+
_spliter=$($SUI client split-coin --coin-id $TALUS_COIN --amounts $SPLIT_AMOUNT)
143+
144+
# Deploy and initialize faucet
115145
echo "Publishing Faucet Contract:"
116-
FaucetContractID=$($SUI client publish --gas-budget 30000000 ./faucet --json | jq -r ".objectChanges[] | select(.packageId) | .packageId")
146+
FaucetContractID=$($SUI client publish ./faucet --json | jq -er ".objectChanges[] | select(.packageId) | .packageId")
117147
sleep 3
118148
echo "Faucet contract at: \"$FaucetContractID\""
119149

120150
echo "Initiate faucet"
121-
FaucetID=$($SUI client call --package $FaucetContractID --module faucet --function initiate \
122-
--type-args $TokenContractID::us::US --type-args 0x2::sui::SUI \
123-
--args $TalusCoin --args $EXCHANGE_RATE --args $WITHDRAWAL_PCT \
124-
--json | jq -r '.objectChanges[] | select(.type == "created") |.objectId')
151+
FaucetID=$($SUI client call --package $FaucetContractID --module faucet --function new \
152+
--type-args $TOKEN_CONTRACT_ID::us::US --type-args 0x2::sui::SUI \
153+
--args $TALUS_COIN --args $EXCHANGE_RATE --args $WITHDRAWAL_PCT \
154+
--json | jq -er '.objectChanges[] | select(.type == "created") |.objectId')
125155
echo "faucet at: $FaucetID"
126156
else
127157
echo "Skipping faucet deployment"
128158
fi
129159

130-
# echo "test mint"
160+
###########################################
161+
# Loyalty Program Setup
162+
###########################################
163+
164+
# Prepare coins for reward pool
165+
sleep 3
166+
echo "Split coin for reward pool"
167+
TALUS_COIN=$($SUI client balance --with-coins --json | jq -er '.[0][][1][] | select(.coinType | contains("::us::US")) | .coinObjectId')
168+
RESERVE_SIZE=$(_calculate_amount "($TOTAL_SUPPLY*85/100)-$SPLIT_AMOUNT")
169+
_spliter=$($SUI client split-coin --coin-id $TALUS_COIN --amounts $RESERVE_SIZE)
170+
171+
# Deploy Loyalty Token Contract
172+
echo "Deploy Loyalty Token Contract"
173+
script=$($SUI client publish ./loyalty --json)
174+
LoyaltyTokenContractID=$(echo $script | jq -er '.objectChanges[] | select(.packageId) | .packageId')
175+
LoyaltyTreasuryCap=$(echo $script | jq -er '.objectChanges[] | select(.objectType!= null and(.objectType | contains("TreasuryCap<"))) | .objectId')
176+
177+
# Deploy reward pool and Deposit Pool
178+
sleep 3
179+
echo "Deploy reward pool and Deposit Pool"
180+
LoyaltyProgramContractID=$($SUI client publish ./deposit_pool --json | jq -er ".objectChanges[] | select(.packageId) | .packageId")
181+
sleep 3
182+
echo "Loyalty Program Contract at: \"$LoyaltyProgramContractID\""
183+
echo "Loyalty Token Contract at: \"$LoyaltyTokenContractID\""
184+
echo "Loyalty Token Cap at: \"$LoyaltyTreasuryCap\""
185+
186+
# Initialize reward pool
187+
echo "Init reward pool and Deposit Pool"
188+
script=$($SUI client call --package $LoyaltyProgramContractID --module deposit_pool --function new \
189+
--type-args $TOKEN_CONTRACT_ID::us::US --type-args $LoyaltyTokenContractID::loyalty::LOYALTY \
190+
--args $LoyaltyTreasuryCap --args $BASE_APY --args false --args 0 \
191+
--json)
192+
ADMIN_CAP=$(echo $script| jq -er '.objectChanges[] | select(.objectType!= null and(.objectType | contains("AdminCap"))) | .objectId')
193+
DEPOSIT_POOL=$(echo $script| jq -er '.objectChanges[] | select(.objectType!= null and(.objectType | contains("DepositPool"))) | .objectId')
194+
echo "Pool at: $DEPOSIT_POOL"
195+
echo "admin cap at $ADMIN_CAP"
196+
197+
# Setup Reward Pool
198+
sleep 3
199+
echo "initiate reward pool"
200+
REWARD_POOL=$($SUI client call --package $LoyaltyProgramContractID --module reward_pool --function new \
201+
--type-args $LoyaltyTokenContractID::loyalty::LOYALTY \
202+
--type-args $TOKEN_CONTRACT_ID::us::US \
203+
--args $TALUS_COIN --args 1 --json | jq -er '.objectChanges[] | select(.objectType!= null and(.objectType | contains("RewardPool"))) | .objectId')
204+
205+
sleep 3
206+
echo "reward pool at $REWARD_POOL"
207+
echo "register reward pool"
208+
209+
# Register reward pool
210+
PolicyID=$($SUI client call --package $LoyaltyProgramContractID --module deposit_pool --function add_reward_program \
211+
--type-args $LoyaltyProgramContractID::reward_pool::RewardProgram \
212+
--type-args $TOKEN_CONTRACT_ID::us::US \
213+
--type-args $LoyaltyTokenContractID::loyalty::LOYALTY \
214+
--args $DEPOSIT_POOL --args $ADMIN_CAP \
215+
--gas-budget 30000000 --json| jq -er '.objectChanges[] | select(.objectType!= null and(.objectType | contains("TokenPolicy"))) | .objectId' )
216+
217+
echo "Policy at $PolicyID"
218+
219+
###########################################
220+
# Test Commands (Commented Out)
221+
###########################################
222+
223+
# Test mint
131224
# $SUI client call --package $FaucetContractID --module faucet --function mint \
132225
# --type-args $TokenContractID::us::US --type-args 0x2::sui::SUI \
133-
# --args $FaucetID --args <sui coin id> \
226+
# --args $FaucetID --args 0x2aecc575afe2859ddd56710c70f9c76845efbb3b4721f30438b60a815814b752 \
134227
# --dry-run
228+
135229
# echo "test refund"
136230
# $SUI client call --package $FaucetContractID --module faucet --function refund \
137231
# --type-args $TokenContractID::us::US --type-args 0x2::sui::SUI \
138232
# --args $FaucetID --args <talus coin id> \
139-
# --dry-run
233+
# --dry-run
234+
235+
# echo "Test deposit to pool"
236+
# $SUI client call --package $LoyaltyProgramContractID --module deposit_pool \
237+
# --function deposit \
238+
# --type-args $TokenContractID::us::US \
239+
# --type-args $LoyaltyTokenContractID::loyalty::LOYALTY \
240+
# --args $DEPOSIT_POOL \
241+
# --args <us coin id> \
242+
# --args 0 --args $USER --args 0x6 --dry-run
243+
244+
# echo "Test withdrawal from pool"
245+
# $SUI client call --package $LoyaltyProgramContractID --module deposit_pool --function withdraw \
246+
# --type-args $TokenContractID::us::US --type-args $LoyaltyTokenContractID::loyalty::LOYALTY \
247+
# --args $DEPOSIT_POOL --args <receipt_nft_id> \
248+
# --gas-budget 10000000
249+
250+
# echo "Test claim reward"
251+
# $SUI client call --package $LoyaltyProgramContractID --module reward_program \
252+
# --function claim \
253+
# --type-args $LoyaltyTokenContractID::loyalty::LOYALTY \
254+
# --type-args $TokenContractID::us::US
255+
# --args $DEPOSIT_POOL --args <token id> --args $PolicyID \

0 commit comments

Comments
 (0)