Skip to main content

Stake

1. Summary

The vePWN Stake contract is an abstract contract inherited by vePWN. It implements functions to manage stake in PWN DAO.

3. Contract details

  • VoteEscrowedPWNStake.sol is written in Solidity version 0.8.25

Features

  • Create, Split, Merge, Increase, Withdraw and Delegate stake

Inherited contracts, implemented Interfaces and ERCs

Functions

createStake

Overview

Function to create a stake.

This function takes two arguments:

  • uint256amount
  • uint256lockUpEpochs

Implementation

function createStake(uint256 amount, uint256 lockUpEpochs) external returns (uint256) {
return createStakeOnBehalfOf(msg.sender, msg.sender, amount, lockUpEpochs);
}
createStakeOnBehalfOf

Overview

Function to create a stake of behalf of another account.

This function takes four arguments:

  • addressstaker
  • addressbeneficiary
  • uint256amount
  • uint256lockUpEpochs

Implementation

function createStakeOnBehalfOf(address staker, address beneficiary, uint256 amount, uint256 lockUpEpochs)
public
returns (uint256 stakeId)
{
// max stake of total initial supply (100M) with decimals 1e26 < max uint88 (3e26)
if (amount < 100 || amount > type(uint88).max) {
revert Error.InvalidAmount();
}
// amount must be a multiple of 100 to prevent rounding errors when computing power
if (amount % 100 > 0) {
revert Error.InvalidAmount();
}
// lock up for <1; 5> + {10} years
if (lockUpEpochs < EPOCHS_IN_YEAR) {
revert Error.InvalidLockUpPeriod();
}
if (lockUpEpochs > 5 * EPOCHS_IN_YEAR && lockUpEpochs != 10 * EPOCHS_IN_YEAR) {
revert Error.InvalidLockUpPeriod();
}

uint16 initialEpoch = epochClock.currentEpoch() + 1;

// store power changes
_updateTotalPower(uint104(amount), initialEpoch, uint8(lockUpEpochs), true);

// create new stake
stakeId = _createStake({
owner: staker,
beneficiary: beneficiary,
initialEpoch: initialEpoch,
amount: uint104(amount),
lockUpEpochs: uint8(lockUpEpochs)
});

// transfer pwn token
pwnToken.transferFrom(msg.sender, address(this), amount);

// emit event
emit StakeCreated(stakeId, staker, beneficiary, amount, lockUpEpochs);
}
splitStake

Overview

Function to split a stake into two. Burns the original stPWN NFT and mints two new ones. The beneficiary of the new stake is also the stake owner.

This function takes three arguments:

  • uint256stakeId
  • addressstakeBeneficiary
  • uint256splitAmount - amount of PWN tokens to split into the first new stake

Implementation

function splitStake(uint256 stakeId, address stakeBeneficiary, uint256 splitAmount)
external
returns (uint256 newStakeId1, uint256 newStakeId2)
{
address staker = msg.sender;
Stake storage originalStake = _stakes[stakeId];
uint16 originalInitialEpoch = originalStake.initialEpoch;
uint104 originalAmount = originalStake.amount;
uint8 originalLockUpEpochs = originalStake.lockUpEpochs;

// split amount must be greater than 0
if (splitAmount == 0) {
revert Error.InvalidAmount();
}
// split amount must be less than stake amount
if (splitAmount >= originalAmount) {
revert Error.InvalidAmount();
}
// split amount must be a multiple of 100 to prevent rounding errors when computing power
if (splitAmount % 100 > 0) {
revert Error.InvalidAmount();
}

// delete original stake
_deleteStake({ owner: staker, beneficiary: stakeBeneficiary, stakeId: stakeId });

// create new stakes
newStakeId1 = _createStake({
owner: staker,
beneficiary: staker,
initialEpoch: originalInitialEpoch,
amount: originalAmount - uint104(splitAmount),
lockUpEpochs: originalLockUpEpochs
});
newStakeId2 = _createStake({
owner: staker,
beneficiary: staker,
initialEpoch: originalInitialEpoch,
amount: uint104(splitAmount),
lockUpEpochs: originalLockUpEpochs
});

// emit event
emit StakeSplit(stakeId, staker, originalAmount - uint104(splitAmount), splitAmount, newStakeId1, newStakeId2);
}
mergeStakes

Overview

Function to merge two stakes into one. Burns both stPWN NFTs and mints a new one. The new stake has lockup period of the first stake with a condition that the first stake lockup must be longer than or equal to the second one. The beneficiary of the new stake is also the stake owner.

This function takes four arguments:

  • uint256stakeId1
  • addressstakeBeneficiary1
  • uint256stakeId2
  • addressstakeBeneficiary2

Implementation

function mergeStakes(uint256 stakeId1, address stakeBeneficiary1, uint256 stakeId2, address stakeBeneficiary2)
external
returns (uint256 newStakeId)
{
address staker = msg.sender;
Stake storage stake1 = _stakes[stakeId1];
Stake storage stake2 = _stakes[stakeId2];
uint16 finalEpoch1 = stake1.initialEpoch + stake1.lockUpEpochs;
uint16 finalEpoch2 = stake2.initialEpoch + stake2.lockUpEpochs;
uint16 newInitialEpoch = epochClock.currentEpoch() + 1;

// the first stake lockup end must be greater than or equal to the second stake lockup end
// both stake lockup ends must be greater than the current epoch
if (finalEpoch1 < finalEpoch2 || finalEpoch1 <= newInitialEpoch) {
revert Error.LockUpPeriodMismatch();
}

uint8 newLockUpEpochs = uint8(finalEpoch1 - newInitialEpoch); // safe cast
// only need to update second stake power changes if has different final epoch
if (finalEpoch1 != finalEpoch2) {
uint104 amount2 = stake2.amount;
// clear second stake power changes if necessary
if (finalEpoch2 > newInitialEpoch) {
_updateTotalPower(amount2, newInitialEpoch, uint8(finalEpoch2 - newInitialEpoch), false);
}
// store new update power changes
_updateTotalPower(amount2, newInitialEpoch, newLockUpEpochs, true);
}

// delete old stakes
_deleteStake({ owner: staker, beneficiary: stakeBeneficiary1, stakeId: stakeId1 });
_deleteStake({ owner: staker, beneficiary: stakeBeneficiary2, stakeId: stakeId2 });

// create new stake
uint104 newAmount = stake1.amount + stake2.amount;
newStakeId = _createStake({
owner: staker,
beneficiary: staker,
initialEpoch: newInitialEpoch,
amount: newAmount,
lockUpEpochs: newLockUpEpochs
});

// emit event
emit StakeMerged(stakeId1, stakeId2, staker, newAmount, newLockUpEpochs, newStakeId);
}
increaseStake

Overview

Function to increase a stake. Both the amount of tokens and the lockup period of a stake can be increased. Old stake NFT is burned and a new one is created during the increase of a stake.

This function takes four arguments:

  • uint256stakeId
  • addressstakeBeneficiary
  • uint256additionalAmount
  • uint256additionalEpochs

Implementation

function increaseStake(uint256 stakeId, address stakeBeneficiary, uint256 additionalAmount, uint256 additionalEpochs)
external
returns (uint256 newStakeId)
{
address staker = msg.sender;
Stake storage stake = _stakes[stakeId];

// additional amount or additional epochs must be greater than 0
if (additionalAmount == 0 && additionalEpochs == 0) {
revert Error.NothingToIncrease();
}
if (additionalAmount > type(uint88).max) {
revert Error.InvalidAmount();
}
// to prevent rounding errors when computing power
if (additionalAmount % 100 > 0) {
revert Error.InvalidAmount();
}
if (additionalEpochs > 10 * EPOCHS_IN_YEAR) {
revert Error.InvalidLockUpPeriod();
}

uint16 newInitialEpoch = epochClock.currentEpoch() + 1;
uint16 oldFinalEpoch = stake.initialEpoch + stake.lockUpEpochs;
uint8 newLockUpEpochs = SafeCast.toUint8(
oldFinalEpoch <= newInitialEpoch ? additionalEpochs : oldFinalEpoch + additionalEpochs - newInitialEpoch
);
// extended lockup must be in <1; 5> + {10} years
if (newLockUpEpochs < EPOCHS_IN_YEAR) {
revert Error.InvalidLockUpPeriod();
}
if (newLockUpEpochs > 5 * EPOCHS_IN_YEAR && newLockUpEpochs != 10 * EPOCHS_IN_YEAR) {
revert Error.InvalidLockUpPeriod();
}

uint104 oldAmount = stake.amount;
uint104 newAmount = oldAmount + uint104(additionalAmount); // safe cast

{ // avoid stack too deep
bool amountAdditionOnly = additionalEpochs == 0;

// clear old power changes if adding epochs
if (!amountAdditionOnly && newLockUpEpochs > additionalEpochs) {
_updateTotalPower(oldAmount, newInitialEpoch, newLockUpEpochs - uint8(additionalEpochs), false);
}

// store new power changes
uint104 amount = amountAdditionOnly ? uint104(additionalAmount) : newAmount;
_updateTotalPower(amount, newInitialEpoch, newLockUpEpochs, true);
}

// delete original stake
_deleteStake({ owner: staker, beneficiary: stakeBeneficiary, stakeId: stakeId });

// create new stake
newStakeId = _createStake({
owner: staker,
beneficiary: staker,
initialEpoch: newInitialEpoch,
amount: newAmount,
lockUpEpochs: newLockUpEpochs
});

// transfer additional PWN tokens
if (additionalAmount > 0) {
pwnToken.transferFrom(staker, address(this), additionalAmount);
}

// emit event
emit StakeIncreased(
stakeId, staker, additionalAmount, newAmount, additionalEpochs, newLockUpEpochs, newStakeId
);
}
withdrawStake

Overview

Function to withdraw a stake. Burns the stake NFT and transfers the underlying PWN tokens to the caller.

This function takes two arguments:

  • uint256stakeId
  • addressstakeBeneficiary

Implementation

function withdrawStake(uint256 stakeId, address stakeBeneficiary) external {
address staker = msg.sender;
Stake storage stake = _stakes[stakeId];

// stake must be unlocked
if (stake.initialEpoch + stake.lockUpEpochs > epochClock.currentEpoch()) {
revert Error.WithrawalBeforeLockUpEnd();
}

// delete stake
_deleteStake({ owner: staker, beneficiary: stakeBeneficiary, stakeId: stakeId });

// transfer pwn tokens to the staker
pwnToken.transfer(staker, stake.amount);

// emit event
emit StakeWithdrawn(stakeId, staker, stake.amount);
}
delegateStakePower

Overview

Function to delegate stakes voting power to another account.

This function takes three arguments:

  • uint256stakeId
  • addresscurrentBeneficiary
  • addressnewBeneficiary

Implementation

function delegateStakePower(uint256 stakeId, address currentBeneficiary, address newBeneficiary) external {
address staker = msg.sender;

// power already delegated to the new beneficiary
if (currentBeneficiary == newBeneficiary) {
revert Error.SameBeneficiary();
}

// staker must be stake owner
_checkIsStakeOwner(staker, stakeId);

// remove token from current beneficiary first to avoid duplicates
_removeStakeFromBeneficiary(stakeId, currentBeneficiary);
_addStakeToBeneficiary(stakeId, newBeneficiary);

// emit event
emit StakePowerDelegated(stakeId, currentBeneficiary, newBeneficiary);
}
getStake

Overview

Function to get information about a stake.

Checkout StakeData struct definition below to learn more about the return type of this function.

This function takes one argument:

  • uint256stakeId

Implementation

function getStake(uint256 stakeId) public view returns (StakeData memory stakeData) {
Stake storage stake = _stakes[stakeId];
uint16 currentEpoch = epochClock.currentEpoch();

stakeData.stakeId = stakeId;
stakeData.owner = stakedPWN.ownerOf(stakeId);
stakeData.initialEpoch = stake.initialEpoch;
stakeData.lockUpEpochs = stake.lockUpEpochs;
stakeData.remainingEpochs = (stakeData.initialEpoch + stakeData.lockUpEpochs >= currentEpoch)
? uint8(stakeData.initialEpoch + stakeData.lockUpEpochs - currentEpoch) : 0;
stakeData.currentMultiplier = (stakeData.initialEpoch <= currentEpoch && stakeData.remainingEpochs > 0)
? uint8(uint104(_power(100, stakeData.remainingEpochs))) : 0;
stakeData.amount = stake.amount;
}
getStakes

Overview

Function to get information about multiple stakes.

Checkout StakeData struct definition below to learn more about the return type of this function.

This function takes one argument:

  • uint256[] calldatastakeIds

Implementation

function getStakes(uint256[] calldata stakeIds) external view returns (StakeData[] memory stakeData) {
stakeData = new StakeData[](stakeIds.length);
for (uint256 i; i < stakeIds.length; ++i) {
stakeData[i] = getStake(stakeIds[i]);
}
}

Events

event StakeCreated(uint256 indexed stakeId, address indexed staker, address indexed beneficiary, uint256 amount, uint256 lockUpEpochs);
event StakeSplit(uint256 indexed stakeId, address indexed staker, uint256 amount1, uint256 amount2, uint256 newStakeId1, uint256 newStakeId2);
event StakeMerged(uint256 indexed stakeId1, uint256 indexed stakeId2, address indexed staker, uint256 amount, uint256 lockUpEpochs, uint256 newStakeId);
event StakeIncreased(uint256 indexed stakeId, address indexed staker, uint256 additionalAmount, uint256 newAmount, uint256 additionalEpochs, uint256 newEpochs, uint256 newStakeId);
event StakeWithdrawn(uint256 indexed stakeId, address indexed staker, uint256 amount);
event StakePowerDelegated(uint256 indexed stakeId, address indexed originalBeneficiary, address indexed newBeneficiary);
StakeCreated

StakeCreated event is emitted when a stake is created.

This event has five parameters:

  • uint256 indexedstakeId
  • address indexedstaker
  • address indexedbeneficiary
  • uint256amount
  • uint256lockUpEpochs
StakeSplit

StakeSplit event is emitted when a stake is split into two.

This event has six parameters:

  • uint256 indexedstakeId
  • address indexedstaker
  • uint256amount1
  • uint256amount2
  • uint256newStakeId1
  • uint256newStakeId2
StakeMerged

StakeMerged event is emitted when two stakes are merged into one.

This event has six parameters:

  • uint256 indexedstakeId1
  • uint256 indexedstakeId2
  • address indexedstaker
  • uint256amount
  • uint256lockUpEpochs
  • uint256newStakeId
StakeIncreased

StakeIncreased event is emitted when a stake is increased. Both the amount of tokens and the lockup period can be increased. Old stake NFT is burned and a new one is created during the increase of a stake.

This event has seven parameters:

  • uint256 indexedstakeId
  • address indexedstaker
  • uint256additionalAmount
  • uint256newAmount
  • uint256additionalEpochs
  • uint256newEpochs
  • uint256newStakeId
StakeWithdrawn

StakeWithdrawn event is emitted when a stake is withdrawn.

This event has three parameters:

  • uint256 indexedstakeId
  • address indexedstaker
  • uint256amount
StakePowerDelegated

StakePowerDelegated event is emitted when stake power is transferred between beneficiaries. When a stake is created, the originalBeneficiary is zero address. When a stake is deleted, the newBeneficiary is zero address.

This event has three parameters:

  • uint256 indexedstakeId
  • address indexedoriginalBeneficiary
  • address indexednewBeneficiary

StakeData Struct

struct StakeData {
uint256 stakeId;
address owner;
uint16 initialEpoch;
uint8 lockUpEpochs;
uint8 remainingEpochs;
uint8 currentMultiplier;
uint104 amount;
}