Skip to main content

ShamaDualStaking.sol

View source on GitHub
Each of the 10 SHAMA pools deploys its own ShamaDualStaking instance. Users stake SHAMA tokens and earn two types of rewards:
  • SHAMA rewards — distributed over a fixed time window using a Synthetix-style accumulator (time-based, continuous)
  • USDT rewards — distributed instantly on deposit by the admin, split pro-rata across all current stakers (snapshot-based)
The contract also supports early staking — the paired ShamaTokenSale contract can stake tokens on behalf of buyers before the reward period officially begins.

Constructor

ParameterTypeDescription
initialOwneraddressAdmin/owner address
_shamaTokenaddressSHAMA token contract
_usdtTokenaddressUSDT token contract
_rewardStartDateuint256Unix timestamp — reward accrual begins
_rewardEndDateuint256Unix timestamp — reward accrual ends
_configStakingConfigStaking parameters (see below)

StakingConfig

struct StakingConfig {
    uint256 minStakeAmount;   // Minimum SHAMA to stake (18-decimal base units)
    uint256 minStakingPeriod; // Minimum lock duration in seconds (e.g. 180 days)
    uint256 maxTotalStake;    // Global cap on total staked SHAMA (0 = no cap)
}

How Rewards Work

SHAMA Rewards (Time-Based)

SHAMA rewards use a Synthetix RewardRate accumulator:
  1. Admin calls addRewardPool(amount) to deposit SHAMA rewards
  2. The contract calculates shamaRewardRate = totalRewards / rewardDuration
  3. Rewards accrue continuously per second, split pro-rata by stake weight
  4. Users claim via claimReward or receive them automatically on unstake
If addRewardPool is called mid-period, the remaining undistributed rewards are combined with the new deposit and the rate is recalculated over the remaining time.

USDT Rewards (Snapshot-Based)

USDT rewards are distributed instantly when deposited:
  1. Admin calls addUsdtReward(amount) — requires at least one active staker
  2. rewardPerTokenStored is updated immediately: += (amount * 1e18) / totalStaked
  3. Each staker’s share is calculated on their next interaction (stake, unstake, claim)
  4. Users claim via claimUsdtReward or receive them automatically on unstake
USDT rewards go only to users who are staked at the moment of the deposit. Late stakers do not retroactively earn past USDT distributions.

User Functions

stake(uint256 amount, address recipient)

Stake SHAMA tokens to start earning rewards.
  • Only callable after rewardStartDate and while staking is active
  • recipient owns the stake (can differ from msg.sender)
  • Must meet minStakeAmount (cumulative — can add to an existing stake)
  • Requires SHAMA approval

earlyStake(uint256 amount, address recipient)

Stake before the reward period starts. Only callable by the paired ShamaTokenSale contract.
  • Enables earlyBuyAndStake flow from the sale contract
  • Tokens are staked but rewards only begin accruing at rewardStartDate

claimReward(address recipient)

Claim accrued SHAMA rewards without unstaking.
  • Rewards are capped at the contract’s available SHAMA balance (minus staked principal)
  • Any excess accrual beyond available balance is preserved for later claim

claimUsdtReward(address recipient)

Claim accrued USDT rewards without unstaking.

unstake(address recipient)

Exit the stake entirely. Returns the staked principal plus all accrued SHAMA and USDT rewards.
  • Reverts if minStakingPeriod hasn’t elapsed (unless staking was cancelled by admin)
  • Cannot unstake before rewardStartDate (unless cancelled)
  • Clears the user’s stake — they can re-stake afterwards

Admin Functions

FunctionDescription
addRewardPool(amount)Deposit SHAMA rewards and set/adjust the reward rate
addUsdtReward(amount)Deposit USDT rewards, distributed instantly to current stakers
startStaking()Re-enable staking (enabled by default on deployment)
stopStaking()Pause new stakes (existing stakers can still unstake and claim)
cancelStaking()Emergency cancel — users can unstake immediately without penalty
setTokenSaleContract(addr)Set which address can call earlyStake
setRewardPeriod(start, end)Update the SHAMA reward window
updateConfig(config)Update min stake, min period, and max total stake
recoverERC20(token, amount)Recover mistakenly sent tokens (SHAMA/USDT protected unless cancelled)
recoverETH()Recover mistakenly sent ETH

View Functions

FunctionReturns
pendingReward(user)Accrued but unclaimed SHAMA reward
pendingUsdtReward(user)Accrued but unclaimed USDT reward
stakes(user)Full StakeInfo struct for a user

StakeInfo

struct StakeInfo {
    uint256 amount;                      // Currently staked SHAMA
    uint256 stakedAt;                    // Timestamp of last stake action
    uint256 accruedShamaReward;          // Pending SHAMA reward
    uint256 userShamaRewardPerTokenPaid; // Accumulator checkpoint (SHAMA)
    uint256 userUsdtRewardPerTokenPaid;  // Accumulator checkpoint (USDT)
    uint256 accruedUsdtReward;           // Pending USDT reward
    uint256 claimedShama;               // Lifetime SHAMA rewards claimed
    uint256 claimedUsdt;                // Lifetime USDT rewards claimed
}

Staker Enumeration

FunctionDescription
stakerCount()Total number of active stakers
stakerAt(index)Staker address at a given index
getStakers(offset, limit)Paginated list of staker addresses
getStakerInfos(offset, limit)Paginated list of StakerSnapshot structs

Integration

Staking Flow

// 1. Approve SHAMA spend
await shamaToken.approve(stakingContract.address, amount);

// 2. Stake
await stakingContract.stake(amount, stakerAddress);

// 3. Check pending rewards
const shamaReward = await stakingContract.pendingReward(stakerAddress);
const usdtReward = await stakingContract.pendingUsdtReward(stakerAddress);

// 4. Claim rewards without unstaking
await stakingContract.claimReward(recipientAddress);
await stakingContract.claimUsdtReward(recipientAddress);

// 5. Unstake (returns principal + all rewards)
await stakingContract.unstake(recipientAddress);

Admin Setup

// 1. Deploy staking contract with reward period and config
// 2. Link to sale contract
await stakingContract.setTokenSaleContract(saleContract.address);

// 3. Deposit SHAMA reward pool
await shamaToken.approve(stakingContract.address, rewardAmount);
await stakingContract.addRewardPool(rewardAmount);

// 4. Periodically deposit USDT from shop revenue
await usdtToken.approve(stakingContract.address, usdtAmount);
await stakingContract.addUsdtReward(usdtAmount);

Events

EventEmitted When
Staked(user, amount, recipient)Tokens staked
Unstaked(user, amount, shamaReward, recipient)Tokens unstaked with rewards
RewardClaimed(user, reward, recipient)SHAMA rewards claimed
UsdtRewardClaimed(user, reward, recipient)USDT rewards claimed
RewardAdded(funder, amount)SHAMA rewards deposited
UsdtRewardAdded(funder, amount)USDT rewards deposited
StakingStarted()Staking re-enabled
StakingStopped()Staking paused
StakingCancelledByAdmin(rewardTokensRecovered)Emergency cancellation
RewardPeriodUpdated(startDate, endDate)Reward window changed
ConfigUpdated(oldConfig, newConfig)Staking config changed

Security

  • ReentrancyGuard on all user-facing functions
  • SafeERC20 for all token transfers
  • SHAMA and USDT tokens are protected from recovery while staking is active
  • Minimum staking period enforced on unstake (bypassed only on admin cancellation)
  • SHAMA reward payouts are capped at available contract balance to prevent insolvency